package web

// Guest-network card (R-319) — the first R-264 reader.
//
// THE FIXTURES BELOW ARE THE REAL WIRE. `liveGuestNetJSON` is the `guest_net` stanza copied verbatim
// out of `demo-felhom-8363b5`'s newest row in the live hub's `host_reports` table on 2026-08-13
// (agent 0.129.0). Testing against a hand-written shape would have proved only that the parser
// matches my own idea of the format — and the defect this whole class comes from (R-260) was exactly
// a hub-side struct that did not match what the agent actually sends.
//
// Every test drives ServeHTTP, so a template gate that never renders is visible here. That is the
// seam-wiring rule: handler tests prove nothing about reachability, and this project has shipped four
// features whose entry point was never wired.

import (
	"strings"
	"testing"
)

// The real stanza: one owned guest, healthy, DHCP, no repairs. `heals_last_hour` and friends are
// ABSENT because the agent marks them `omitempty` and this box has never needed a repair — which is
// itself the shape scenario A has to survive.
const liveGuestNetJSON = `{
  "host": {"cpu_percent": 4.0, "memory_percent": 30.0, "disk_percent": 20.0},
  "guest_net": {
    "checked_at": "2026-08-13T08:15:33Z",
    "guests": [
      {"vmid": 9201, "state": "healthy", "mode": "dhcp", "ip": "192.168.0.149",
       "has_route": true, "dhclient_alive": true, "checked_at": "2026-08-13T08:14:30Z",
       "message": "address, default route and dhclient all present"}
    ]
  }
}`

// The same box after the watchdog has had to keep fixing it — the R-54 heal fields populated. This is
// the shape the incident of 2026-07-20 would have produced had the watchdog existed then.
const repairingGuestNetJSON = `{
  "host": {"cpu_percent": 4.0, "memory_percent": 30.0, "disk_percent": 20.0},
  "guest_net": {
    "checked_at": "2026-08-13T08:15:33Z",
    "guests": [
      {"vmid": 9201, "state": "healthy", "mode": "dhcp", "ip": "192.168.0.149",
       "has_route": true, "dhclient_alive": true, "checked_at": "2026-08-13T08:14:30Z",
       "healed": true, "heal_succeeded": true, "last_heal_at": "2026-08-13T08:09:12Z",
       "heals_last_hour": 6, "message": "dhclient was absent; restarted"}
    ]
  }
}`

// A report from a capable agent with NO guest_net stanza at all — the watchdog switched off, or a
// report predating the feature on this box.
const silentGuestNetJSON = `{"host": {"cpu_percent": 4.0, "memory_percent": 30.0, "disk_percent": 20.0}}`

// The stanza arrives, but its contents are not what the hub expects: `guests` is an object where an
// array belongs, and `heals_last_hour` is a string. This is what a wire drift or a truncated write
// looks like from the hub's side.
const malformedGuestNetJSON = `{
  "host": {"cpu_percent": 4.0},
  "guest_net": {"checked_at": "2026-08-13T08:15:33Z", "guests": {"vmid": "nine-two-oh-one"}}
}`

// ── A — a machine reporting healthy guest networking, no repairs → shown as healthy ─────────────
//
// WRONG OUTCOME GUARDED: an empty or alarming state on a machine that is fine. A card that cried
// unknown on every healthy box would be switched off within a week, and then the B case below would
// never be seen either.
func TestGuestNet_A_HealthyRendersHealthy(t *testing.T) {
	s, st, _ := newRevealServer(t)
	cookie, _ := newRevealSession(t, s)
	seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", liveGuestNetJSON, "")

	body := getHostPage(t, s, cookie, "demo-felhom-8363b5")

	if !strings.Contains(body, "Guest network") {
		t.Fatal("no Guest network card — the reader shows nothing")
	}
	if !strings.Contains(body, "192.168.0.149") {
		t.Error("the guest's address did not reach the page")
	}
	if !strings.Contains(body, "badge-ok") {
		t.Error("a healthy guest must render as healthy, not as an empty or alarming state")
	}
	if strings.Contains(body, "needs attention") {
		t.Error("a machine that is fine is being alarmed on")
	}
	// The unknown branches must NOT fire for a box that reported properly.
	if strings.Contains(body, "does not run the guest-network watchdog") ||
		strings.Contains(body, "reported no guest-network state") {
		t.Error("a reporting box rendered one of the unknown sentences")
	}
}

// ── B — a machine whose watchdog has repaired the guest repeatedly ──────────────────────────────
//
// WRONG OUTCOME GUARDED: a green tick because the CURRENT state is fine. This is the exact shape of
// the failed-disk-drawn-as-a-healthy-empty-disk defect, and it is the reason this card exists at all:
// `state` says "healthy" in this fixture, because between repairs it IS.
func TestGuestNet_B_RepeatedRepairsAreVisible(t *testing.T) {
	s, st, _ := newRevealServer(t)
	cookie, _ := newRevealSession(t, s)
	seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", repairingGuestNetJSON, "")

	body := getHostPage(t, s, cookie, "demo-felhom-8363b5")

	if !strings.Contains(body, "needs attention") {
		t.Fatal("a guest repaired 6 times in an hour is reported as fine — the whole point of the card")
	}
	if !strings.Contains(body, "had their networking repaired in the last hour") {
		t.Error("the climbing-repairs sentence is missing")
	}
	if !strings.Contains(body, ">6<") {
		t.Error("the repair COUNT is not rendered; 'some repairs' is not the signal, the number is")
	}
	if !strings.Contains(body, "2026-08-13T08:09:12Z") {
		t.Error("the last repair time is not offered (it is in the title attribute)")
	}
}

// ── C — a machine that does not report the fact at all → drawn as unknown, never healthy ────────
//
// WRONG OUTCOME GUARDED: absence read as good news. THE ONE THAT MATTERS. Two distinct absences are
// checked because they need different words: an agent that cannot report it, and a capable agent that
// said nothing.
func TestGuestNet_C_SilentIsUnknownNotHealthy(t *testing.T) {
	t.Run("capable agent, no stanza", func(t *testing.T) {
		s, st, _ := newRevealServer(t)
		cookie, _ := newRevealSession(t, s)
		seedNetHost(t, st, "demo-hp-bb76ea", "0.129.0", silentGuestNetJSON, "")

		body := getHostPage(t, s, cookie, "demo-hp-bb76ea")

		if !strings.Contains(body, "reported no guest-network state") {
			t.Fatal("a silent box must SAY it is unknown")
		}
		if strings.Contains(body, "badge-ok") && strings.Contains(body, "Guest network") {
			assertNoHealthyBadgeInGuestNetCard(t, body)
		}
	})

	t.Run("agent too old to report it", func(t *testing.T) {
		s, st, _ := newRevealServer(t)
		cookie, _ := newRevealSession(t, s)
		// 0.91.0 predates the R-54 watchdog (v0.92.0): the absence is expected AND still unknown.
		seedNetHost(t, st, "old-agent-box", "0.91.0", silentGuestNetJSON, "")

		body := getHostPage(t, s, cookie, "old-agent-box")

		if !strings.Contains(body, "does not run the guest-network watchdog") {
			t.Fatal("an old agent's silence must be named as an old agent's silence")
		}
		if !strings.Contains(body, "<strong>unknown</strong>, not healthy") {
			t.Error("the unknown-is-not-healthy sentence is missing")
		}
		assertNoHealthyBadgeInGuestNetCard(t, body)
	})
}

// ── D — the fact arrives malformed → unknown, and the hub does not 500 ──────────────────────────
//
// WRONG OUTCOME GUARDED: a page that breaks on one machine's bad field. getHostPage fails the test on
// any non-200, so surviving the call IS half the assertion.
func TestGuestNet_D_MalformedIsUnknownAndDoesNotBreakThePage(t *testing.T) {
	s, st, _ := newRevealServer(t)
	cookie, _ := newRevealSession(t, s)
	seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", malformedGuestNetJSON, "")

	body := getHostPage(t, s, cookie, "demo-felhom-8363b5") // 200 or the test dies here

	if !strings.Contains(body, "reported no guest-network state") {
		t.Fatal("a malformed stanza must degrade to unknown")
	}
	assertNoHealthyBadgeInGuestNetCard(t, body)
}

// A guest whose state string is one the watchdog never asserts (a future value, or a truncated field)
// is drawn as unknown rather than falling through to the healthy branch. The switch in
// guestNetGuestView.Unknown is an ALLOW-LIST for exactly this reason: an unrecognised value defaults
// to unknown, and adding a state to the agent cannot silently paint it green here.
func TestGuestNet_UnrecognisedStateIsUnknown(t *testing.T) {
	s, st, _ := newRevealServer(t)
	cookie, _ := newRevealSession(t, s)
	seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", `{
      "host": {"cpu_percent": 1.0},
      "guest_net": {"checked_at": "2026-08-13T08:15:33Z",
        "guests": [{"vmid": 9201, "state": "quantum", "mode": "dhcp", "ip": "10.0.0.5"}]}}`, "")

	body := getHostPage(t, s, cookie, "demo-felhom-8363b5")

	if !strings.Contains(body, "partly unknown") {
		t.Fatal("an unrecognised guest state must summarise as unknown, not healthy")
	}
}

// The empty-list case is deliberately NOT the same as silence: the agent's own contract says a stanza
// with a fresh checked_at and no guests means "the watchdog ran and found nothing to report", and that
// must stay distinguishable from "the watchdog is not wired" — the shape the v0.91.0 inert seam hid
// behind.
func TestGuestNet_EmptyListIsNotSilence(t *testing.T) {
	s, st, _ := newRevealServer(t)
	cookie, _ := newRevealSession(t, s)
	seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0",
		`{"host": {"cpu_percent": 1.0}, "guest_net": {"checked_at": "2026-08-13T08:15:33Z"}}`, "")

	body := getHostPage(t, s, cookie, "demo-felhom-8363b5")

	if !strings.Contains(body, "The watchdog ran and has no guest to report") {
		t.Fatal("an empty guest list must say the watchdog RAN")
	}
	if strings.Contains(body, "reported no guest-network state") {
		t.Error("an empty list was collapsed into silence — the two are different facts")
	}
}

// assertNoHealthyBadgeInGuestNetCard checks that the Guest network card contains no healthy badge.
// It slices the card out of the page rather than searching the whole body, because `badge-ok` occurs
// all over a host page (capabilities, storage, WireGuard) and a whole-page search would pass
// vacuously — which is the "instrument that can drop results silently" trap.
func assertNoHealthyBadgeInGuestNetCard(t *testing.T, body string) {
	t.Helper()
	i := strings.Index(body, "Guest network")
	if i < 0 {
		t.Fatal("no Guest network card to slice — the assertion would have passed vacuously")
	}
	card := body[i:]
	if j := strings.Index(card, "</section>"); j >= 0 {
		card = card[:j]
	} else {
		t.Fatal("could not find the end of the Guest network card")
	}
	if strings.Contains(card, "badge-ok") {
		t.Errorf("an UNKNOWN guest-network state rendered a healthy badge:\n%s", card)
	}
}

// A repair that was ATTEMPTED and FAILED is a harder fact than a repair that worked, and the count
// alone cannot express it: six successful repairs is a nuisance, six failed ones is a guest that is
// down right now. Dropping `heal_succeeded` from the decoder would be R-260 exactly — the OOB decoder
// mirrored five of eight fields and the three it dropped included the deciding one.
func TestGuestNet_FailedRepairIsDistinctFromFrequentRepair(t *testing.T) {
	s, st, _ := newRevealServer(t)
	cookie, _ := newRevealSession(t, s)
	seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", `{
      "host": {"cpu_percent": 1.0},
      "guest_net": {"checked_at": "2026-08-13T08:15:33Z",
        "guests": [{"vmid": 9201, "state": "unhealthy", "mode": "dhcp",
                    "has_route": false, "dhclient_alive": false,
                    "healed": true, "heal_succeeded": false, "heals_last_hour": 3,
                    "message": "dhclient restart did not restore the lease"}]}}`, "")

	body := getHostPage(t, s, cookie, "demo-felhom-8363b5")

	if !strings.Contains(body, "repair failed") {
		t.Fatal("a repair that was attempted and failed is not distinguished from one that worked")
	}
	if !strings.Contains(body, "needs attention") {
		t.Error("a guest whose repair failed must summarise as needing attention")
	}
	assertNoHealthyBadgeInGuestNetCard(t, body)
}
