package templates

import (
	"html/template"
	"mal/internal/domain"
	"testing"
)

func TestDictEvenArgs(t *testing.T) {
	t.Parallel()

	got, err := dict("key1", "val1", "key2", 42)
	if err != nil {
		t.Fatalf("dict error: %v", err)
	}
	if got["key1"] != "val1" {
		t.Errorf("expected key1=val1, got %v", got["key1"])
	}
	if got["key2"] != 42 {
		t.Errorf("expected key2=42, got %v", got["key2"])
	}
}

func TestDictOddArgs(t *testing.T) {
	t.Parallel()

	_, err := dict("key1", "val1", "key2")
	if err == nil {
		t.Fatal("expected error for odd number of args")
	}
}

func TestDictNonStringKey(t *testing.T) {
	t.Parallel()

	_, err := dict(42, "val1")
	if err == nil {
		t.Fatal("expected error for non-string key")
	}
}

func TestJSONAttr(t *testing.T) {
	t.Parallel()

	v := map[string]int{"a": 1}
	got, err := jsonAttr(v)
	if err != nil {
		t.Fatalf("jsonAttr error: %v", err)
	}
	want := template.HTMLAttr(`{"a":1}`)
	if got != want {
		t.Errorf("expected %q, got %q", want, got)
	}
}

func TestJSONAttrUnmarshalable(t *testing.T) {
	t.Parallel()

	_, err := jsonAttr(func() {})
	if err == nil {
		t.Fatal("expected error for unmarshalable value")
	}
}

func TestBrowseURLPreservesAndOverridesParams(t *testing.T) {
	t.Parallel()

	got, err := browseURL(
		map[string]any{
			"Query":   "full metal",
			"Type":    "tv",
			"Status":  "airing",
			"OrderBy": "score",
			"Sort":    "desc",
			"Studio":  42,
			"SFW":     true,
			"Genres":  []int{1, 2},
			"Page":    3,
		},
		map[string]any{
			"status": "",
			"sort":   "asc",
			"page":   4,
		},
	)
	if err != nil {
		t.Fatalf("browseURL error: %v", err)
	}

	want := "/browse?genres=1&genres=2&order_by=score&page=4&q=full+metal&sfw=true&sort=asc&studio=42&type=tv"
	if got != want {
		t.Fatalf("unexpected url\nwant: %s\ngot:  %s", want, got)
	}
}

func TestBrowseURLClearsAndEncodesValues(t *testing.T) {
	t.Parallel()

	got, err := browseURL(
		map[string]any{
			"Query":  "K-On! & friends",
			"Studio": 99,
			"SFW":    false,
			"Genres": []int{7, 9},
		},
		map[string]any{
			"studio": "",
			"genres": []int{5},
		},
	)
	if err != nil {
		t.Fatalf("browseURL error: %v", err)
	}

	want := "/browse?genres=5&q=K-On%21+%26+friends&sfw=false"
	if got != want {
		t.Fatalf("unexpected url\nwant: %s\ngot:  %s", want, got)
	}
}

func TestBrowseURLWithoutSFWStateOmitsParameter(t *testing.T) {
	t.Parallel()

	got, err := browseURL(
		map[string]any{
			"Query": "monster",
		},
		nil,
	)
	if err != nil {
		t.Fatalf("browseURL error: %v", err)
	}

	if got != "/browse?q=monster" {
		t.Fatalf("expected sfw to be omitted, got %s", got)
	}
}

func TestBrowseURLIncludesSFWTrue(t *testing.T) {
	t.Parallel()

	got, err := browseURL(
		map[string]any{
			"Query": "monster",
			"SFW":   true,
		},
		nil,
	)
	if err != nil {
		t.Fatalf("browseURL error: %v", err)
	}

	if got != "/browse?q=monster&sfw=true" {
		t.Fatalf("expected sfw=true, got %s", got)
	}
}

func TestBrowseURLSFWFalse(t *testing.T) {
	t.Parallel()

	got, err := browseURL(
		map[string]any{
			"Query": "monster",
			"SFW":   false,
		},
		nil,
	)
	if err != nil {
		t.Fatalf("browseURL error: %v", err)
	}

	if got != "/browse?q=monster&sfw=false" {
		t.Fatalf("expected sfw=false, got %s", got)
	}
}

func TestBrowseURLWithSFWOverride(t *testing.T) {
	t.Parallel()

	got, err := browseURL(
		map[string]any{
			"Query": "monster",
		},
		map[string]any{
			"sfw": true,
		},
	)
	if err != nil {
		t.Fatalf("browseURL error: %v", err)
	}

	if got != "/browse?q=monster&sfw=true" {
		t.Fatalf("unexpected url, got %s", got)
	}
}

func TestSeqPositive(t *testing.T) {
	t.Parallel()

	got := seq(5)
	want := []int{0, 1, 2, 3, 4}
	if len(got) != len(want) {
		t.Fatalf("expected length %d, got %d", len(want), len(got))
	}
	for i := range want {
		if got[i] != want[i] {
			t.Errorf("expected [%d]=%d, got %d", i, want[i], got[i])
		}
	}
}

func TestSeqZero(t *testing.T) {
	t.Parallel()

	got := seq(0)
	if len(got) != 0 {
		t.Errorf("expected empty slice, got %v", got)
	}
}

func TestSeqNegative(t *testing.T) {
	t.Parallel()

	got := seq(-3)
	if len(got) != 0 {
		t.Errorf("expected empty slice, got %v", got)
	}
}

func TestDivNormal(t *testing.T) {
	t.Parallel()

	got := div(10, 3)
	if got != 10.0/3.0 {
		t.Errorf("expected %f, got %f", 10.0/3.0, got)
	}
}

func TestDivByZero(t *testing.T) {
	t.Parallel()

	got := div(5, 0)
	if got != 0 {
		t.Errorf("expected 0, got %f", got)
	}
}

func TestCeilDivNormal(t *testing.T) {
	t.Parallel()

	got := ceilDiv(10, 3)
	if got != 4 {
		t.Errorf("expected 4, got %d", got)
	}
}

func TestCeilDivExact(t *testing.T) {
	t.Parallel()

	got := ceilDiv(9, 3)
	if got != 3 {
		t.Errorf("expected 3, got %d", got)
	}
}

func TestCeilDivByZero(t *testing.T) {
	t.Parallel()

	got := ceilDiv(5, 0)
	if got != 0 {
		t.Errorf("expected 0, got %d", got)
	}
}

func TestIDivNormal(t *testing.T) {
	t.Parallel()

	got := idiv(10, 3)
	if got != 3 {
		t.Errorf("expected 3, got %d", got)
	}
}

func TestIDivByZero(t *testing.T) {
	t.Parallel()

	got := idiv(5, 0)
	if got != 0 {
		t.Errorf("expected 0, got %d", got)
	}
}

func TestAtoiString(t *testing.T) {
	t.Parallel()

	got := atoi("42")
	if got != 42 {
		t.Errorf("expected 42, got %d", got)
	}
}

func TestAtoiInvalid(t *testing.T) {
	t.Parallel()

	got := atoi("not-a-number")
	if got != 0 {
		t.Errorf("expected 0, got %d", got)
	}
}

func TestAtoiNonString(t *testing.T) {
	t.Parallel()

	got := atoi(42)
	if got != 0 {
		t.Errorf("expected 0, got %d", got)
	}
}

func TestToIntFromInt(t *testing.T) {
	t.Parallel()

	got := toInt(42)
	if got != 42 {
		t.Errorf("expected 42, got %d", got)
	}
}

func TestToIntFromString(t *testing.T) {
	t.Parallel()

	got := toInt("42")
	if got != 42 {
		t.Errorf("expected 42, got %d", got)
	}
}

func TestToIntFromInvalidString(t *testing.T) {
	t.Parallel()

	got := toInt("bad")
	if got != 0 {
		t.Errorf("expected 0, got %d", got)
	}
}

func TestToIntFromFloat(t *testing.T) {
	t.Parallel()

	got := toInt(3.14)
	if got != 3 {
		t.Errorf("expected 3, got %d", got)
	}
}

func TestPercentNormal(t *testing.T) {
	t.Parallel()

	got := percent(25, 100)
	if got != 25 {
		t.Errorf("expected 25, got %f", got)
	}
}

func TestPercentZeroTotal(t *testing.T) {
	t.Parallel()

	got := percent(25, 0)
	if got != 0 {
		t.Errorf("expected 0, got %f", got)
	}
}

func TestFormatDateRFC3339(t *testing.T) {
	t.Parallel()

	got := formatDate("2024-01-15T10:30:00Z")
	want := "Jan 15, 2024"
	if got != want {
		t.Errorf("expected %q, got %q", want, got)
	}
}

func TestFormatDateAltFormat(t *testing.T) {
	t.Parallel()

	got := formatDate("2024-01-15T10:30:00+00:00")
	want := "Jan 15, 2024"
	if got != want {
		t.Errorf("expected %q, got %q", want, got)
	}
}

func TestFormatDateInvalid(t *testing.T) {
	t.Parallel()

	got := formatDate("not-a-date")
	if got != "not-a-date" {
		t.Errorf("expected input string back, got %q", got)
	}
}

func TestNextSortAsc(t *testing.T) {
	t.Parallel()

	got := nextSort("asc")
	if got != "desc" {
		t.Errorf("expected desc, got %s", got)
	}
}

func TestNextSortDesc(t *testing.T) {
	t.Parallel()

	got := nextSort("desc")
	if got != "asc" {
		t.Errorf("expected asc, got %s", got)
	}
}

func TestNextSortDefault(t *testing.T) {
	t.Parallel()

	got := nextSort("")
	if got != "asc" {
		t.Errorf("expected asc, got %s", got)
	}
}

func TestGenresParams(t *testing.T) {
	t.Parallel()

	got := genresParams([]int{1, 2, 3})
	want := "genres=1&genres=2&genres=3"
	if got != want {
		t.Errorf("expected %q, got %q", want, got)
	}
}

func TestGenresParamsEmpty(t *testing.T) {
	t.Parallel()

	got := genresParams(nil)
	if got != "" {
		t.Errorf("expected empty, got %q", got)
	}
}

func TestOrderedGenreOptionsSelectedFirst(t *testing.T) {
	t.Parallel()

	got := orderedGenreOptions(
		[]domain.Genre{
			{MalID: 1, Name: "Action"},
			{MalID: 2, Name: "Comedy"},
			{MalID: 3, Name: "Shounen"},
			{MalID: 4, Name: "Drama"},
		},
		[]int{3, 1},
	)

	wantIDs := []int{1, 3, 2, 4}
	if len(got) != len(wantIDs) {
		t.Fatalf("expected %d genres, got %d", len(wantIDs), len(got))
	}
	for i, wantID := range wantIDs {
		if got[i].Genre.MalID != wantID {
			t.Fatalf("genre at index %d = %d, want %d", i, got[i].Genre.MalID, wantID)
		}
	}
	if !got[0].Selected || !got[1].Selected {
		t.Fatalf("expected first two genres to be selected, got %+v", got[:2])
	}
	if !got[2].StartsInactive {
		t.Fatalf("expected first inactive genre to start divider, got %+v", got[2])
	}
	if got[3].StartsInactive {
		t.Fatalf("expected divider only on first inactive genre, got %+v", got[3])
	}
}

func TestOrderedGenreOptionsNoSelectedGenresPreservesOrder(t *testing.T) {
	t.Parallel()

	got := orderedGenreOptions(
		[]domain.Genre{
			{MalID: 1, Name: "Action"},
			{MalID: 2, Name: "Comedy"},
		},
		nil,
	)

	for i, option := range got {
		if option.Selected {
			t.Fatalf("expected genre at index %d to be inactive", i)
		}
		if option.StartsInactive {
			t.Fatalf("expected no inactive divider without active genres")
		}
		if option.Genre.MalID != i+1 {
			t.Fatalf("genre at index %d = %d, want %d", i, option.Genre.MalID, i+1)
		}
	}
}

func TestPosterURLWebp(t *testing.T) {
	t.Parallel()

	got := posterURL("https://example.com/webp", "https://example.com/jpg", 400, 600)
	want := "https://example.com/webp"
	if got != want {
		t.Errorf("expected %q, got %q", want, got)
	}
}

func TestPosterURLJpgFallback(t *testing.T) {
	t.Parallel()

	got := posterURL(nil, "https://example.com/jpg", 400, 600)
	want := "https://example.com/jpg"
	if got != want {
		t.Errorf("expected %q, got %q", want, got)
	}
}

func TestPosterURLPlaceholder(t *testing.T) {
	t.Parallel()

	got := posterURL(nil, nil, 400, 600)
	want := "https://placehold.co/400x600?text=No+Image"
	if got != want {
		t.Errorf("expected %q, got %q", want, got)
	}
}

func TestPosterURLEmptyWebp(t *testing.T) {
	t.Parallel()

	got := posterURL("", "", 200, 300)
	want := "https://placehold.co/200x300?text=No+Image"
	if got != want {
		t.Errorf("expected %q, got %q", want, got)
	}
}

func TestEpisodeRangeStart(t *testing.T) {
	t.Parallel()

	tests := []struct {
		epNum, step, want int
	}{
		{1, 100, 1},
		{50, 100, 1},
		{100, 100, 1},
		{101, 100, 101},
		{156, 100, 101},
		{200, 100, 101},
		{201, 100, 201},
	}
	for _, tc := range tests {
		got := episodeRangeStart(tc.epNum, tc.step)
		if got != tc.want {
			t.Errorf("episodeRangeStart(%d, %d) = %d, want %d", tc.epNum, tc.step, got, tc.want)
		}
	}
}

func TestEpisodeRangeStartZero(t *testing.T) {
	t.Parallel()

	got := episodeRangeStart(0, 100)
	if got != 1 {
		t.Errorf("expected 1, got %d", got)
	}
}
