package api

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/ponzischeme89/memby/server/internal/store"
)

func documentJSON(t *testing.T, values map[string]any) []byte {
	t.Helper()
	raw, err := json.Marshal(normalizePreferences(values))
	if err != nil {
		t.Fatalf("marshal document: %v", err)
	}
	return raw
}

// The whole point of the history table: one line per setting, in the words the catalogue
// uses, rather than two JSON documents for somebody to compare by eye.
func TestPreferenceChangesDescribesOneSetting(t *testing.T) {
	before := normalizePreferences(map[string]any{"homeCardDensity": "standard"})
	after := normalizePreferences(map[string]any{"homeCardDensity": "large"})

	changes := preferenceChanges(before, after)
	if len(changes) != 1 {
		t.Fatalf("got %d changes, want 1: %+v", len(changes), changes)
	}
	if changes[0].Key != "homeCardDensity" {
		t.Errorf("key = %q, want homeCardDensity", changes[0].Key)
	}
	if changes[0].Before != "Standard" || changes[0].After != "Large" {
		t.Errorf("got %q → %q, want Standard → Large", changes[0].Before, changes[0].After)
	}
}

// A television pushing back the document it already held is an ordinary event. Listing it
// as a change would fill the history with rows nobody made.
func TestPreferenceChangesIgnoresIdenticalDocuments(t *testing.T) {
	document := normalizePreferences(map[string]any{"subtitlesEnabled": false})
	if changes := preferenceChanges(document, document); len(changes) != 0 {
		t.Fatalf("got %d changes for an unchanged document: %+v", len(changes), changes)
	}
}

// A key the catalogue no longer knows is not a change anybody can act on, and a value
// stored before a setting existed must not read as one appearing out of nothing.
func TestPreferenceChangesIgnoresUnknownKeys(t *testing.T) {
	before := map[string]any{"retiredSetting": "one"}
	after := map[string]any{"retiredSetting": "two"}
	if changes := preferenceChanges(before, after); len(changes) != 0 {
		t.Fatalf("an unknown key produced changes: %+v", changes)
	}
}

// Order is the setting for a multi, so a reordered list is a change even though the same
// rows are ticked.
func TestPreferenceChangesNoticesReorderedRows(t *testing.T) {
	before := normalizePreferences(map[string]any{
		"homeSections": []any{"continue", "favorites", "latest"}})
	after := normalizePreferences(map[string]any{
		"homeSections": []any{"favorites", "continue", "latest"}})
	changes := preferenceChanges(before, after)
	if len(changes) != 1 || changes[0].Key != "homeSections" {
		t.Fatalf("got %+v, want one homeSections change", changes)
	}
}

func TestPreferenceValueLabelWordsEveryKind(t *testing.T) {
	for _, testCase := range []struct {
		key   string
		value any
		want  string
	}{
		{"showTitleLogo", true, "On"},
		{"showTitleLogo", false, "Off"},
		{"homeArtworkStyle", "backdrop", "Backdrops"},
		{"seekIntervalSeconds", 30, "30 seconds"},
		{"forYouMinutes", 0, "No limit"},
		{"homeSections", []string{"latest", "continue"}, "Latest movies, Continue watching"},
		{"homeHiddenRows", []string{}, "None"},
		{"homeHiddenRows", []string{"recommended"}, "recommended"},
	} {
		definition, ok := preferenceDefinitionFor(testCase.key)
		if !ok {
			t.Fatalf("%s is not in the catalogue", testCase.key)
		}
		got := preferenceValueLabel(definition, normalizePreference(definition, testCase.value))
		if got != testCase.want {
			t.Errorf("%s = %q, want %q", testCase.key, got, testCase.want)
		}
	}
}

// The history arrives newest first, so a row's diff is against the element *after* it. Get
// this backwards and every line in the table reads inverted.
func TestAdminRevisionsDiffsAgainstThePrecedingRevision(t *testing.T) {
	history := []store.PreferenceRevision{
		{Revision: 3, Preferences: documentJSON(t, map[string]any{"homeCardDensity": "large"}),
			Source: "admin", CreatedAt: time.Now()},
		{Revision: 2, Preferences: documentJSON(t, map[string]any{"homeCardDensity": "compact"}),
			Source: "device", DeviceName: "Lounge", CreatedAt: time.Now()},
		{Revision: 1, Preferences: documentJSON(t, nil), Source: "device", CreatedAt: time.Now()},
	}

	rows := adminRevisions(history, 3)
	if len(rows) != 3 {
		t.Fatalf("got %d rows, want 3", len(rows))
	}
	if !rows[0].Current {
		t.Error("the newest revision is not marked current")
	}
	if len(rows[0].Changes) != 1 || rows[0].Changes[0].Before != "Compact" ||
		rows[0].Changes[0].After != "Large" {
		t.Fatalf("r3 changes = %+v, want compact → large", rows[0].Changes)
	}
	// The oldest entry held has nothing below it, so it is labelled rather than diffed
	// against the defaults — which would claim decisions nobody made.
	if !rows[2].Initial || len(rows[2].Changes) != 0 {
		t.Errorf("r1 initial = %v with %d changes, want true with 0",
			rows[2].Initial, len(rows[2].Changes))
	}
	if rows[0].Author != "Admin console" || rows[1].Author != "Lounge" {
		t.Errorf("authors = %q, %q; want Admin console, Lounge", rows[0].Author, rows[1].Author)
	}
	// A write from before the history recorded a device name must not be attributed to
	// an invented one.
	if rows[2].Author != "A television" {
		t.Errorf("nameless device author = %q, want A television", rows[2].Author)
	}
}

// A restore is announced as what it is, and the row it came from stays named.
func TestAdminRevisionsCarriesRestoreProvenance(t *testing.T) {
	history := []store.PreferenceRevision{
		{Revision: 5, Preferences: documentJSON(t, nil), Source: "admin", RestoredFrom: 2},
		{Revision: 4, Preferences: documentJSON(t, nil), Source: "device"},
	}
	rows := adminRevisions(history, 5)
	if rows[0].RestoredFrom != 2 {
		t.Errorf("restoredFrom = %d, want 2", rows[0].RestoredFrom)
	}
}

// "Behind" is what the page colours by, and a set holding something newer than the current
// revision is an anomaly rather than a negative number to render.
func TestBehindByNeverGoesNegative(t *testing.T) {
	for _, testCase := range []struct{ current, held, want int64 }{
		{7, 7, 0}, {7, 4, 3}, {7, 9, 0}, {7, 0, 7},
	} {
		if got := behindBy(testCase.current, testCase.held); got != testCase.want {
			t.Errorf("behindBy(%d, %d) = %d, want %d",
				testCase.current, testCase.held, got, testCase.want)
		}
	}
}
