package monitor

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

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

func reportWithHeal(healedAt string) []byte {
	if healedAt == "" {
		return []byte(`{"host_id":"h1","mgmt_plane":{"privsep_dir_ok":true,"sshd_reachable":true}}`)
	}
	return []byte(`{"host_id":"h1","mgmt_plane":{"privsep_dir_ok":true,"sshd_reachable":true,"healed_recently":true,"privsep_healed_at":"` + healedAt + `"}}`)
}

func newMgmtStore(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
}

// A NEW heal timestamp fires exactly one mgmt_plane_healed; the first observation only seeds; a repeat
// of the same timestamp does not re-fire. Companion TestHostMgmtPlaneChecker_NoHealNoEvent proves it's
// the heal, not the sweep, that fires it (drop the emit → this test fails).
func TestHostMgmtPlaneChecker_NewHealAlertsOnce(t *testing.T) {
	st := newMgmtStore(t)
	// first report already carries a heal marker → seed baseline, NO event on construction.
	st.SaveHostReport("h1", "c1", reportWithHeal("2026-07-05T16:42:17Z"), store.HostReportDenorm{})
	var events []string
	mc := NewHostMgmtPlaneChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	if mc.GetState("h1") != "2026-07-05T16:42:17Z" {
		t.Fatalf("seed = %q", mc.GetState("h1"))
	}
	if len(events) != 0 {
		t.Fatalf("construction must not emit, got %v", events)
	}

	// a NEW heal (different timestamp) → one warning.
	st.SaveHostReport("h1", "c1", reportWithHeal("2026-07-05T18:00:00Z"), store.HostReportDenorm{})
	mc.Check()
	if len(events) != 1 || events[0] != "mgmt_plane_healed" {
		t.Fatalf("new heal → one mgmt_plane_healed, got %v", events)
	}
	// same timestamp again → no duplicate.
	mc.Check()
	if len(events) != 1 {
		t.Fatalf("same heal must not re-emit, got %v", events)
	}
}

// The live-drill path: the hub started while the host was HEALTHY (no marker seeded), then the
// watchdog auto-heals and the marker appears in a later report → the FIRST observation alerts (a heal
// is an event, not a baseline). This is the case a pure trust-on-first-report would have missed.
func TestHostMgmtPlaneChecker_FirstHealAfterHealthyAlerts(t *testing.T) {
	st := newMgmtStore(t)
	st.SaveHostReport("h1", "c1", reportWithHeal(""), store.HostReportDenorm{}) // healthy at construction
	var events []string
	mc := NewHostMgmtPlaneChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	if len(events) != 0 || mc.GetState("h1") != "" {
		t.Fatalf("healthy construction: no seed/event, got state=%q events=%v", mc.GetState("h1"), events)
	}
	st.SaveHostReport("h1", "c1", reportWithHeal("2026-07-05T17:07:39Z"), store.HostReportDenorm{}) // heal happens
	mc.Check()
	if len(events) != 1 || events[0] != "mgmt_plane_healed" {
		t.Fatalf("first heal after healthy must alert once, got %v", events)
	}
	mc.Check() // same marker → no duplicate
	if len(events) != 1 {
		t.Fatalf("no duplicate on the same marker, got %v", events)
	}
}

// A marker PRESENT at construction is seeded silently (avoid a false alarm on hub restart for a heal
// that predates the hub watching) — the construction-no-event half of NewHealAlertsOnce, isolated.
func TestHostMgmtPlaneChecker_PreexistingMarkerSeededSilently(t *testing.T) {
	st := newMgmtStore(t)
	st.SaveHostReport("h1", "c1", reportWithHeal("2026-07-05T09:00:00Z"), store.HostReportDenorm{})
	var events []string
	mc := NewHostMgmtPlaneChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	mc.Check() // same marker still latest → no alert (it was seeded)
	if len(events) != 0 {
		t.Fatalf("a marker present at construction must NOT alert (startup false-alarm guard), got %v", events)
	}
}

func TestHostMgmtPlaneChecker_NoHealNoEvent(t *testing.T) {
	st := newMgmtStore(t)
	st.SaveHostReport("h1", "c1", reportWithHeal(""), store.HostReportDenorm{}) // healthy, no marker
	var events []string
	mc := NewHostMgmtPlaneChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	mc.Check()
	mc.Check()
	if len(events) != 0 {
		t.Fatalf("a healthy host (no heal marker) must never alert, got %v", events)
	}
	if mc.GetState("h1") != "" {
		t.Fatalf("no marker → no baseline, got %q", mc.GetState("h1"))
	}
}

// A recurring clobber: heal at T1 (seed), heal again at T2 (alert), heal again at T3 (alert) — each
// distinct heal surfaces, which is the whole point (find the recurring cause before a lockout).
func TestHostMgmtPlaneChecker_RecurringHealsEachAlert(t *testing.T) {
	st := newMgmtStore(t)
	st.SaveHostReport("h1", "c1", reportWithHeal("2026-07-05T10:00:00Z"), store.HostReportDenorm{})
	var events []string
	mc := NewHostMgmtPlaneChecker(st, func(_, et, _, _, _, _ string) { events = append(events, et) }, log.New(io.Discard, "", 0))
	st.SaveHostReport("h1", "c1", reportWithHeal("2026-07-05T11:00:00Z"), store.HostReportDenorm{})
	mc.Check()
	st.SaveHostReport("h1", "c1", reportWithHeal("2026-07-05T12:00:00Z"), store.HostReportDenorm{})
	mc.Check()
	if len(events) != 2 {
		t.Fatalf("two distinct new heals after seed → two alerts, got %d (%v)", len(events), events)
	}
}
