package monitor

import (
	"io"
	"log"
	"path/filepath"
	"testing"

	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
	_ "modernc.org/sqlite"
)

const (
	fpA = "60b5974d586f5f3c8ec41eb998d0f07406178219c36bf6d3ff377570279d8245"
	fpB = "911d703c9cb4cf54d7aba9d9a768e587e9d0757ea638b1db595939623c675738"
)

func reportWithLeaf(fp string) []byte {
	return []byte(`{"host_id":"h1","leaf_fingerprint":"` + fp + `"}`)
}

func newLeafStore(t *testing.T) *store.Store {
	t.Helper()
	st, err := store.New(filepath.Join(t.TempDir(), "test.db"), log.New(io.Discard, "", 0))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { st.Close() })
	st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ck", RetrievalPassword: "p"})
	st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"})
	return st
}

// B.2 change RED-PROOF: seed fp A, then fp B → exactly one host_leaf_changed (A→B), baseline advanced
// to B. Companion (TestHostLeafChecker_NoChangeNoEvent): fp stays A → no event — proving the CHANGE,
// not the cycle, fires it.
func TestHostLeafChecker_ChangeAlertsOnce(t *testing.T) {
	st := newLeafStore(t)
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpA), store.HostReportDenorm{})
	var events []string
	lc := NewHostLeafChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	if lc.GetState("h1") != fpA {
		t.Fatalf("seed baseline = %q, want %q", lc.GetState("h1"), fpA)
	}
	if len(events) != 0 {
		t.Fatalf("construction must not emit, got %v", events)
	}

	st.SaveHostReport("h1", "c1", reportWithLeaf(fpB), store.HostReportDenorm{})
	lc.Check()
	if len(events) != 1 || events[0] != "host_leaf_changed" {
		t.Fatalf("fp change → one host_leaf_changed, got %v", events)
	}
	if lc.GetState("h1") != fpB {
		t.Fatalf("baseline not advanced, got %q want %q", lc.GetState("h1"), fpB)
	}
	lc.Check() // steady at B → no duplicate
	if len(events) != 1 {
		t.Fatalf("steady fp must not re-emit, got %v", events)
	}
}

func TestHostLeafChecker_NoChangeNoEvent(t *testing.T) {
	st := newLeafStore(t)
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpA), store.HostReportDenorm{})
	var events []string
	lc := NewHostLeafChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpA), store.HostReportDenorm{}) // same fp
	lc.Check()
	if len(events) != 0 {
		t.Fatalf("unchanged fp must not alert, got %v", events)
	}
}

// First observation seeds the baseline silently.
func TestHostLeafChecker_FirstObsSeedsNoEvent(t *testing.T) {
	st := newLeafStore(t)
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpA), store.HostReportDenorm{})
	var events []string
	lc := NewHostLeafChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	// construction already seeded; a Check with no change stays silent.
	lc.Check()
	if len(events) != 0 {
		t.Fatalf("first-obs/seed must be silent, got %v", events)
	}
	if lc.GetState("h1") != fpA {
		t.Fatalf("baseline = %q, want %q", lc.GetState("h1"), fpA)
	}
}

// A change-BACK (B→A) is also a change → another event (informative).
func TestHostLeafChecker_ChangeBackReAlerts(t *testing.T) {
	st := newLeafStore(t)
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpA), store.HostReportDenorm{})
	var events []string
	lc := NewHostLeafChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpB), store.HostReportDenorm{})
	lc.Check() // A→B
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpA), store.HostReportDenorm{})
	lc.Check() // B→A
	if len(events) != 2 {
		t.Fatalf("change-back should re-alert (want 2), got %d", len(events))
	}
}

// An empty reported fp (pre-v0.48.0 / local-API disabled) is unknown — never seeded, never an alert,
// and never overwrites an existing baseline.
func TestHostLeafChecker_EmptyFpSkipped(t *testing.T) {
	st := newLeafStore(t)
	st.SaveHostReport("h1", "c1", []byte(`{"host_id":"h1"}`), store.HostReportDenorm{}) // no leaf_fingerprint
	var events []string
	lc := NewHostLeafChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	if lc.GetState("h1") != "" {
		t.Fatalf("empty fp must not seed a baseline, got %q", lc.GetState("h1"))
	}
	lc.Check()
	if len(events) != 0 {
		t.Fatalf("empty fp must not alert, got %v", events)
	}
	// A real fp later → seeds (no alert, it's the first real baseline).
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpA), store.HostReportDenorm{})
	lc.Check()
	if len(events) != 0 || lc.GetState("h1") != fpA {
		t.Fatalf("first real fp should seed silently, events=%v state=%q", events, lc.GetState("h1"))
	}
}

// A blocked customer's host is dropped — no event.
func TestHostLeafChecker_CustomerBlockedDropped(t *testing.T) {
	st := newLeafStore(t)
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpA), store.HostReportDenorm{})
	var events []string
	lc := NewHostLeafChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	if err := st.SetCustomerConfigStatus("c1", "blocked"); err != nil {
		t.Fatalf("block customer: %v", err)
	}
	st.SaveHostReport("h1", "c1", reportWithLeaf(fpB), store.HostReportDenorm{}) // would-be change
	lc.Check()
	if len(events) != 0 {
		t.Fatalf("blocked customer must not alert, got %v", events)
	}
	if lc.GetState("h1") != "" {
		t.Fatalf("blocked host should be dropped, got %q", lc.GetState("h1"))
	}
}
