// dashboard_filter_test.go — covers the NS-04 filter + sort pipeline
// in pure-Go form, without going through HTTP. The handler tests
// elsewhere prove end-to-end render; here we focus on edge cases of
// the column-sort + filter precedence so a regression in either is
// surfaced loudly.
package http

import (
	"net/url"
	"testing"
	"time"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
	"gitea.dcglab.co.uk/steve/restic-manager/internal/version"
)

func makeFilterHosts() []store.Host {
	t1 := time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC)
	t2 := time.Date(2026, 5, 4, 12, 0, 0, 0, time.UTC)
	tSeen := time.Date(2026, 5, 5, 12, 0, 0, 0, time.UTC)
	return []store.Host{
		{
			ID: "01HHA", Name: "alpha", OS: "linux", Status: "online",
			RepoStatus: "ready", Tags: []string{"prod"}, SnapshotCount: 30,
			LastBackupAt: &t1, LastSeenAt: &tSeen, RepoSizeBytes: 1000,
		},
		{
			ID: "01HHB", Name: "bravo", OS: "linux", Status: "offline",
			RepoStatus: "init_failed", Tags: []string{"dev"}, SnapshotCount: 10,
			LastBackupAt: &t2, LastSeenAt: &tSeen, RepoSizeBytes: 5000,
		},
		{
			ID: "01HHC", Name: "charlie", OS: "windows", Status: "online",
			RepoStatus: "unknown", Tags: []string{"prod", "edge"}, SnapshotCount: 0,
			LastSeenAt: nil, // never_seen path
		},
	}
}

// TestFilterAndSortDashboardSearchAndStatus covers the precedence of
// search ∧ status as combined filters.
func TestFilterAndSortDashboardSearchAndStatus(t *testing.T) {
	t.Parallel()
	hosts := makeFilterHosts()

	// status=online narrows to alpha + charlie.
	got := filterAndSortDashboardHosts(hosts, dashboardFilter{Status: "online", Sort: "name", Dir: "asc"})
	if len(got) != 2 || got[0].Name != "alpha" || got[1].Name != "charlie" {
		t.Errorf("status=online: got %d names %v, want [alpha charlie]", len(got), namesOf(got))
	}
	// q=bra narrows to bravo regardless of status default.
	got = filterAndSortDashboardHosts(hosts, dashboardFilter{Search: "bra", Sort: "name", Dir: "asc"})
	if len(got) != 1 || got[0].Name != "bravo" {
		t.Errorf("search=bra: got %v", namesOf(got))
	}
	// repo_status=init_failed narrows to bravo only.
	got = filterAndSortDashboardHosts(hosts, dashboardFilter{RepoStatus: "init_failed", Sort: "name", Dir: "asc"})
	if len(got) != 1 || got[0].Name != "bravo" {
		t.Errorf("repo_status=init_failed: got %v", namesOf(got))
	}
	// status=never_seen narrows on LastSeenAt == nil → charlie only.
	got = filterAndSortDashboardHosts(hosts, dashboardFilter{Status: "never_seen", Sort: "name", Dir: "asc"})
	if len(got) != 1 || got[0].Name != "charlie" {
		t.Errorf("status=never_seen: got %v", namesOf(got))
	}
	// tag=prod narrows to alpha + charlie.
	got = filterAndSortDashboardHosts(hosts, dashboardFilter{Tag: "prod", Sort: "name", Dir: "asc"})
	if len(got) != 2 || got[0].Name != "alpha" || got[1].Name != "charlie" {
		t.Errorf("tag=prod: got %v", namesOf(got))
	}
}

// TestSortDashboardHostsColumns verifies each meaningful column
// sorts as expected, both ascending and descending.
func TestSortDashboardHostsColumns(t *testing.T) {
	t.Parallel()
	hosts := makeFilterHosts()

	cases := []struct {
		col, dir string
		want     []string
	}{
		{"name", "asc", []string{"alpha", "bravo", "charlie"}},
		{"name", "desc", []string{"charlie", "bravo", "alpha"}},
		{"snapshot_count", "asc", []string{"charlie", "bravo", "alpha"}},
		{"snapshot_count", "desc", []string{"alpha", "bravo", "charlie"}},
		{"last_backup", "asc", []string{"charlie", "alpha", "bravo"}}, // nil → zero → first
		{"repo_status", "asc", []string{"bravo", "alpha", "charlie"}}, // init_failed < ready < unknown
	}
	for _, c := range cases {
		c := c
		t.Run(c.col+"_"+c.dir, func(t *testing.T) {
			got := append([]store.Host(nil), hosts...)
			sortDashboardHosts(got, c.col, c.dir)
			if names := namesOf(got); !sliceEq(names, c.want) {
				t.Errorf("got %v, want %v", names, c.want)
			}
		})
	}
}

// TestFilterAndSortDashboardUpdatesBehind: ?updates=behind narrows
// to hosts whose agent_version is non-empty AND != server's version.
func TestFilterAndSortDashboardUpdatesBehind(t *testing.T) {
	t.Parallel()
	hosts := []store.Host{
		{ID: "01a", Name: "alpha", AgentVersion: "v0.0.1", Status: "online"},
		{ID: "01b", Name: "bravo", AgentVersion: version.Version, Status: "online"},
		{ID: "01c", Name: "charlie", AgentVersion: "", Status: "online"}, // never seen
		{ID: "01d", Name: "delta", AgentVersion: "v0.0.1", Status: "offline"},
	}
	got := filterAndSortDashboardHosts(hosts, dashboardFilter{Updates: "behind", Sort: "name", Dir: "asc"})
	// alpha + delta both behind; bravo (current) and charlie (empty) excluded.
	if len(got) != 2 || got[0].Name != "alpha" || got[1].Name != "delta" {
		t.Errorf("updates=behind: got %v", namesOf(got))
	}
}

// TestParseDashboardFilterDefaults: empty query gives sort=name asc.
func TestParseDashboardFilterDefaults(t *testing.T) {
	t.Parallel()
	f := parseDashboardFilter(url.Values{})
	if f.Sort != "name" || f.Dir != "asc" {
		t.Errorf("defaults: got sort=%q dir=%q, want name/asc", f.Sort, f.Dir)
	}
}

// TestBuildDashboardSortURLsToggles: clicking the active column
// flips direction; clicking another column resets to asc.
func TestBuildDashboardSortURLsToggles(t *testing.T) {
	t.Parallel()
	active := dashboardFilter{Sort: "name", Dir: "asc"}
	urls := buildDashboardSortURLs(active)
	if got := urls["name"]; got != "/?dir=desc" {
		t.Errorf("name URL on active asc: got %q, want /?dir=desc", got)
	}
	// Switching to a non-default column also drops dir=asc since asc
	// is the encoded default.
	if got := urls["last_backup"]; got != "/?sort=last_backup" {
		t.Errorf("last_backup URL: got %q, want /?sort=last_backup", got)
	}
}

func namesOf(hs []store.Host) []string {
	out := make([]string, len(hs))
	for i, h := range hs {
		out[i] = h.Name
	}
	return out
}

func sliceEq(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
