package api

import (
	"encoding/json"
	"reflect"
	"testing"
)

// The document a television applies must always be complete. A missing key is a setting
// with no value on the other end, which is how a launcher ends up with no rows.
func TestNormalizePreferencesFillsEveryKnownKey(t *testing.T) {
	result := normalizePreferences(nil)
	if len(result) != len(preferenceCatalogue) {
		t.Fatalf("got %d keys, want %d", len(result), len(preferenceCatalogue))
	}
	for _, definition := range preferenceCatalogue {
		if _, ok := result[definition.Key]; !ok {
			t.Errorf("%s missing from a normalised document", definition.Key)
		}
	}
}

func TestNormalizePreferencesDropsUnknownKeys(t *testing.T) {
	result := normalizePreferences(map[string]any{"embyToken": "secret", "showTitleLogo": false})
	if _, ok := result["embyToken"]; ok {
		t.Error("an unknown key survived normalisation")
	}
	if result["showTitleLogo"] != false {
		t.Errorf("showTitleLogo = %v, want false", result["showTitleLogo"])
	}
}

func TestNormalizePreferencesRejectsIllegalValues(t *testing.T) {
	result := normalizePreferences(map[string]any{
		"homeCardDensity":  "enormous",
		"showRatingsStrip": "yes",
		"forYouMinutes":    45,
		"homeSections":     []any{"nonsense"},
	})
	if result["homeCardDensity"] != "standard" {
		t.Errorf("homeCardDensity = %v, want the default", result["homeCardDensity"])
	}
	if result["showRatingsStrip"] != true {
		t.Errorf("showRatingsStrip = %v, want the default", result["showRatingsStrip"])
	}
	if result["forYouMinutes"] != 0 {
		t.Errorf("forYouMinutes = %v, want the default", result["forYouMinutes"])
	}
	// Every named row was unknown, which leaves nothing to draw — the default stands in
	// rather than an empty launcher being honoured as a choice.
	want := []string{"continue", "favorites", "latest"}
	if !reflect.DeepEqual(result["homeSections"], want) {
		t.Errorf("homeSections = %v, want %v", result["homeSections"], want)
	}
}

// The order of a multi-select is the setting, not an implementation detail: it is the
// order the rows appear in on the launcher.
func TestNormalizePreferencesKeepsMultiOrderAndDedupes(t *testing.T) {
	result := normalizePreferences(map[string]any{
		"homeSections": []any{"latest", "continue", "latest", "unknown"},
	})
	want := []string{"latest", "continue"}
	if !reflect.DeepEqual(result["homeSections"], want) {
		t.Errorf("homeSections = %v, want %v", result["homeSections"], want)
	}
}

// Free-form row ids are stored newline-separated on the television, so an id containing
// one would come back as two rows on the next sync.
func TestNormalizePreferencesRejectsNewlinesInRowIds(t *testing.T) {
	result := normalizePreferences(map[string]any{
		"homeRowOrder": []any{"recommended", "bad\nid", "  ", "recommended", "latest"},
	})
	want := []string{"recommended", "latest"}
	if !reflect.DeepEqual(result["homeRowOrder"], want) {
		t.Errorf("homeRowOrder = %v, want %v", result["homeRowOrder"], want)
	}
}

func TestNormalizePreferencesBoundsListLength(t *testing.T) {
	ids := make([]any, 0, maxListEntries+50)
	for i := 0; i < maxListEntries+50; i++ {
		ids = append(ids, string(rune('a'+i%26))+string(rune('a'+i/26)))
	}
	result := normalizePreferences(map[string]any{"homeHiddenRows": ids})
	if got := len(result["homeHiddenRows"].([]string)); got > maxListEntries {
		t.Errorf("kept %d ids, want at most %d", got, maxListEntries)
	}
}

// A default that is a slice must be copied out, or a caller mutating one user's document
// edits the catalogue for every user for the life of the process.
func TestNormalizePreferencesDoesNotShareSliceDefaults(t *testing.T) {
	first := normalizePreferences(nil)
	first["homeSections"].([]string)[0] = "tampered"
	second := normalizePreferences(nil)
	if second["homeSections"].([]string)[0] != "continue" {
		t.Fatal("mutating one document changed the catalogue default")
	}
}

// Numbers arrive from encoding/json as float64; a document that has been through the wire
// must normalise identically to one built in Go.
func TestNormalizePreferencesAcceptsJSONNumbers(t *testing.T) {
	var decoded map[string]any
	if err := json.Unmarshal([]byte(`{"forYouMinutes":60}`), &decoded); err != nil {
		t.Fatal(err)
	}
	if got := normalizePreferences(decoded)["forYouMinutes"]; got != 60 {
		t.Errorf("forYouMinutes = %v, want 60", got)
	}
}

// The client applies whatever comes back, so a round trip through storage must be a
// fixed point — otherwise two televisions could disagree about what they just agreed on.
func TestNormalizePreferencesIsIdempotentThroughJSON(t *testing.T) {
	once := normalizePreferences(map[string]any{
		"homeSections": []any{"favorites"}, "homeCardDensity": "large",
		"hideWatchedMovies": true, "homeRowOrder": []any{"recommended"},
	})
	raw, err := json.Marshal(once)
	if err != nil {
		t.Fatal(err)
	}
	twice := decodePreferences(raw)
	rawOnce, _ := json.Marshal(once)
	rawTwice, _ := json.Marshal(twice)
	if string(rawOnce) != string(rawTwice) {
		t.Errorf("round trip changed the document:\n once: %s\ntwice: %s", rawOnce, rawTwice)
	}
}

// The catalogue is the contract the admin console and the television both read. A default
// that is not itself a legal value would hand every new viewer something the editor
// cannot represent.
func TestPreferenceCatalogueDefaultsAreLegal(t *testing.T) {
	seen := map[string]bool{}
	for _, definition := range preferenceCatalogue {
		if seen[definition.Key] {
			t.Errorf("%s appears in the catalogue twice", definition.Key)
		}
		seen[definition.Key] = true
		if definition.Name == "" || definition.Area == "" {
			t.Errorf("%s needs a name and an area to render in the console", definition.Key)
		}
		normalised := normalizePreference(definition, defaultValue(definition))
		if !reflect.DeepEqual(normalised, defaultValue(definition)) {
			t.Errorf("%s default %v is not a legal value (normalises to %v)",
				definition.Key, definition.Default, normalised)
		}
	}
}

// The skip interval is a number from a fixed list, and the television normalises it a
// second time. Both ends have to agree on which values exist, or a viewer's choice comes
// back as the default the moment they change some other setting.
func TestNormalizePreferencesSeekInterval(t *testing.T) {
	if got := normalizePreferences(map[string]any{"seekIntervalSeconds": 30})["seekIntervalSeconds"]; got != 30 {
		t.Errorf("seekIntervalSeconds = %v, want 30", got)
	}
	// 15 is not offered; neither is a string. Both fall back rather than reaching a player
	// as a step size nothing on the television knows how to label.
	for _, value := range []any{15, "30", 0, nil} {
		got := normalizePreferences(map[string]any{"seekIntervalSeconds": value})["seekIntervalSeconds"]
		if got != 10 {
			t.Errorf("seekIntervalSeconds for %v = %v, want the default 10", value, got)
		}
	}
}
