package recognize

import (
	"strings"
	"testing"
)

func intp(n int) *int { return &n }

func inputWith(paths ...string) Input {
	files := make([]File, len(paths))
	for i, p := range paths {
		files[i] = File{Path: p, Size: 1 << 20}
	}
	return Input{Files: files}
}

func TestValidateSchema_OK(t *testing.T) {
	in := inputWith("a.mkv", "b.mkv")
	p := Plan{
		Type:  MediaSeries,
		Title: "Show",
		Files: []PlanFile{
			{Src: "a.mkv", Role: RoleEpisode, Season: intp(1), Episode: intp(1)},
			{Src: "b.mkv", Role: RoleEpisode, Season: intp(1), Episode: intp(2)},
		},
	}
	if err := validateSchema(&p, in); err != nil {
		t.Fatalf("validateSchema: %v", err)
	}
}

func TestValidateSchema_Errors(t *testing.T) {
	in := inputWith("a.mkv")
	tests := []struct {
		name string
		p    Plan
		want string
	}{
		{"empty type", Plan{Title: "x", Files: []PlanFile{{Src: "a.mkv", Role: RoleMain}}}, "type is empty"},
		{"bad type", Plan{Type: "show", Title: "x", Files: []PlanFile{{Src: "a.mkv", Role: RoleMain}}}, "unknown type"},
		{"empty title", Plan{Type: MediaMovie, Files: []PlanFile{{Src: "a.mkv", Role: RoleMain}}}, "title is empty"},
		{"no files", Plan{Type: MediaMovie, Title: "x"}, "files list is empty"},
		{"bad role", Plan{Type: MediaMovie, Title: "x", Files: []PlanFile{{Src: "a.mkv", Role: "boss"}}}, "unknown role"},
		{"empty src", Plan{Type: MediaMovie, Title: "x", Files: []PlanFile{{Src: "", Role: RoleMain}}}, "empty src"},
		{"unknown src", Plan{Type: MediaMovie, Title: "x", Files: []PlanFile{{Src: "z.mkv", Role: RoleMain}}}, "not found among torrent files"},
		{"episode no num", Plan{Type: MediaSeries, Title: "x", Files: []PlanFile{{Src: "a.mkv", Role: RoleEpisode, Season: intp(1)}}}, "has no episode number"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateSchema(&tt.p, in)
			if err == nil || !strings.Contains(err.Error(), tt.want) {
				t.Errorf("err = %v, want contains %q", err, tt.want)
			}
		})
	}
}

func TestParsePlan_FencedJSON(t *testing.T) {
	in := inputWith("film.mkv")
	raw := "Вот результат:\n```json\n{\"type\":\"movie\",\"title\":\"Film\"," +
		"\"files\":[{\"src\":\"film.mkv\",\"role\":\"main\"}]}\n```"
	p, err := parsePlan(raw, in, testLogger())
	if err != nil {
		t.Fatalf("parsePlan: %v", err)
	}
	if p.Title != "Film" || p.Type != MediaMovie {
		t.Errorf("plan = %+v", p)
	}
}

func TestParsePlan_UnknownFieldTolerated(t *testing.T) {
	in := inputWith("film.mkv")
	raw := `{"type":"movie","title":"Film","extra_field":123,
		"files":[{"src":"film.mkv","role":"main"}]}`
	if _, err := parsePlan(raw, in, testLogger()); err != nil {
		t.Fatalf("unknown field should be tolerated: %v", err)
	}
}

func TestStructuralWarnings_Movie(t *testing.T) {
	twoMains := Plan{Type: MediaMovie, Files: []PlanFile{
		{Role: RoleMain}, {Role: RoleMain},
	}}
	if w := structuralWarnings(twoMains); len(w) != 1 || !strings.Contains(w[0], "ожидался ровно 1") {
		t.Errorf("warnings = %v", w)
	}

	noMain := Plan{Type: MediaMovie, Files: []PlanFile{{Role: RoleSample}}}
	if w := structuralWarnings(noMain); len(w) != 1 {
		t.Errorf("want 1 warning for 0 mains, got %v", w)
	}

	clean := Plan{Type: MediaMovie, Files: []PlanFile{{Role: RoleMain}, {Role: RoleSample}}}
	if w := structuralWarnings(clean); len(w) != 0 {
		t.Errorf("clean movie should have no warnings, got %v", w)
	}
}

func TestSeriesWarnings_GapAndDup(t *testing.T) {
	files := []PlanFile{
		{Role: RoleEpisode, Season: intp(1), Episode: intp(1)},
		{Role: RoleEpisode, Season: intp(1), Episode: intp(1)}, // дубль
		{Role: RoleEpisode, Season: intp(1), Episode: intp(4)}, // пропуск 2,3
	}
	w := seriesWarnings(files)
	var dup, gap bool
	for _, s := range w {
		if strings.Contains(s, "дубль") {
			dup = true
		}
		if strings.Contains(s, "пропуск") {
			gap = true
		}
	}
	if !dup || !gap {
		t.Errorf("want dup and gap warnings, got %v", w)
	}
}

func TestSeriesWarnings_Clean(t *testing.T) {
	files := []PlanFile{
		{Role: RoleEpisode, Season: intp(1), Episode: intp(1)},
		{Role: RoleEpisode, Season: intp(1), Episode: intp(2)},
		{Role: RoleEpisode, Season: intp(2), Episode: intp(1)},
	}
	if w := seriesWarnings(files); len(w) != 0 {
		t.Errorf("clean series should have no warnings, got %v", w)
	}
}

func TestConsistencyWarnings(t *testing.T) {
	yearMismatch := consistencyWarnings(
		Plan{Type: MediaMovie, Year: 2001},
		PreParse{Year: 1999},
	)
	if len(yearMismatch) != 1 || !strings.Contains(yearMismatch[0], "год расходится") {
		t.Errorf("warnings = %v", yearMismatch)
	}

	typeMismatch := consistencyWarnings(
		Plan{Type: MediaMovie},
		PreParse{Season: 2},
	)
	if len(typeMismatch) != 1 || !strings.Contains(typeMismatch[0], "тип расходится") {
		t.Errorf("warnings = %v", typeMismatch)
	}

	agree := consistencyWarnings(
		Plan{Type: MediaSeries, Year: 2006},
		PreParse{Year: 2006, Season: 2},
	)
	if len(agree) != 0 {
		t.Errorf("agreeing signals should not warn, got %v", agree)
	}
}

func TestDecide_MetadataDisabled(t *testing.T) {
	p := Plan{Type: MediaMovie, Title: "X", Confidence: 0.99, Files: []PlanFile{{Role: RoleMain}}}
	d := decide(p, PreParse{}, nil, false, 0.85)
	if d.Auto {
		t.Error("без метабаз авто недопустимо")
	}
	if len(d.Reasons) == 0 || !strings.Contains(d.Reasons[0], "метабазы отключены") {
		t.Errorf("first reason should be DB match, got %v", d.Reasons)
	}
}

func TestDecide_NoMatch(t *testing.T) {
	p := Plan{Type: MediaMovie, Title: "X", Confidence: 0.99, Files: []PlanFile{{Role: RoleMain}}}
	d := decide(p, PreParse{}, nil, true, 0.85)
	if d.Auto || !strings.Contains(d.Reasons[0], "не найдено в базе") {
		t.Errorf("reasons = %v", d.Reasons)
	}
}

func TestDecide_AutoMovie(t *testing.T) {
	p := Plan{Type: MediaMovie, Title: "The Matrix", Year: 1999, Confidence: 0.95,
		Files: []PlanFile{{Role: RoleMain}, {Role: RoleSample}}}
	match := &Match{Provider: "tmdb", ProviderID: "603", Title: "The Matrix", Year: 1999}
	d := decide(p, PreParse{Year: 1999}, match, true, 0.85)
	if !d.Auto {
		t.Errorf("clean movie with match must be auto, reasons: %v", d.Reasons)
	}
}

func TestDecide_LowConfidenceBlocksAuto(t *testing.T) {
	p := Plan{Type: MediaMovie, Title: "X", Year: 2000, Confidence: 0.5,
		Files: []PlanFile{{Role: RoleMain}}}
	match := &Match{Provider: "tmdb", ProviderID: "1", Title: "X", Year: 2000}
	d := decide(p, PreParse{}, match, true, 0.85)
	if d.Auto || !hasReason(d.Reasons, "уверенность") {
		t.Errorf("low confidence must block auto, reasons: %v", d.Reasons)
	}
}

func TestDecide_AutoSeriesEpisodeCount(t *testing.T) {
	s := 2
	mk := func(e int) PlanFile { ep := e; return PlanFile{Role: RoleEpisode, Season: &s, Episode: &ep} }
	p := Plan{Type: MediaSeries, Title: "Fargo", Year: 2014, Confidence: 0.9,
		Files: []PlanFile{mk(1), mk(2), mk(3)}}
	match := &Match{Provider: "tmdb", ProviderID: "1", Title: "Fargo", Year: 2014,
		SeasonEpisodeCounts: map[int]int{2: 3}}
	if d := decide(p, PreParse{Year: 2014}, match, true, 0.85); !d.Auto {
		t.Errorf("full season must be auto, reasons: %v", d.Reasons)
	}

	// Неполный пак (в базе 10) — авто блокируется.
	match.SeasonEpisodeCounts = map[int]int{2: 10}
	if d := decide(p, PreParse{Year: 2014}, match, true, 0.85); d.Auto ||
		!hasReason(d.Reasons, "распознано серий 3, в базе 10") {
		t.Errorf("partial pack must block auto, reasons: %v", d.Reasons)
	}

	// Нет данных о числе серий — авто блокируется.
	match.SeasonEpisodeCounts = nil
	if d := decide(p, PreParse{Year: 2014}, match, true, 0.85); d.Auto ||
		!hasReason(d.Reasons, "нет данных о числе серий") {
		t.Errorf("missing counts must block auto, reasons: %v", d.Reasons)
	}
}

func TestPreParse(t *testing.T) {
	pre := preParse("The.Matrix.1999.1080p.BluRay.x264")
	if pre.Year != 1999 {
		t.Errorf("year = %d, want 1999", pre.Year)
	}
	if !strings.Contains(strings.ToLower(pre.Title), "matrix") {
		t.Errorf("title = %q", pre.Title)
	}

	series := preParse("Some.Show.S02E05.720p")
	if series.Season != 2 || series.Episode != 5 {
		t.Errorf("season/episode = %d/%d, want 2/5", series.Season, series.Episode)
	}
}
