package web

import (
	"testing"

	"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)

// R-170 — the drive-backed boot recreate gate reads the customer's recorded intent instead of
// counting containers, so the two boot gates stop disagreeing about the same question.

const rgDrive = "/mnt/felhom-drives/hdd_1"

func rgPresent() map[string]bool { return map[string]bool{rgDrive: true} }

// --- Group D / Scenario E — the three-way table, every row ----------------------------------------

func TestShouldRecreateOnBoot_IntentTable(t *testing.T) {
	// RED-PROOF: restore the old `&& hasContainers` ending (i.e. ignore `desired` entirely) and the
	// `running/no containers` row fails — that row is the whole point of R-170, and it is the shape a
	// power cut mid-compose leaves behind on a drive-backed app.
	// Demonstrated in REPORT.md §4.
	cases := []struct {
		name          string
		desired       string
		hasContainers bool
		want          bool
	}{
		{"stopped + no containers", stacks.DesiredStateStopped, false, false},
		{"stopped + containers present", stacks.DesiredStateStopped, true, false},
		{"running + no containers", stacks.DesiredStateRunning, false, true},
		{"running + containers present", stacks.DesiredStateRunning, true, true},
		{"legacy + no containers", stacks.DesiredStateUnknown, false, false},
		{"legacy + containers present", stacks.DesiredStateUnknown, true, true},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := shouldRecreateOnBoot(true, rgDrive, rgPresent(), c.hasContainers, c.desired)
			if got != c.want {
				t.Fatalf("shouldRecreateOnBoot(desired=%q hasContainers=%v) = %v, want %v",
					c.desired, c.hasContainers, got, c.want)
			}
		})
	}
}

func TestShouldRecreateOnBoot_StoppedIsNeverRecreated_WhateverElseIsTrue(t *testing.T) {
	// The safety property R-55 added this gate's filter for, restated on the new signal: a customer's
	// Stop must survive a guest reboot. It must hold across every other axis.
	for _, hasContainers := range []bool{true, false} {
		if shouldRecreateOnBoot(true, rgDrive, rgPresent(), hasContainers, stacks.DesiredStateStopped) {
			t.Fatalf("a recorded Stop was recreated (hasContainers=%v) — this silently undoes the "+
				"customer's decision on every guest reboot, which is the defect R-55 existed to fix",
				hasContainers)
		}
	}
}

func TestShouldRecreateOnBoot_DriveAbsentStillWinsOverIntent(t *testing.T) {
	// The `presentStable` term is load-bearing and must NOT have been dropped in sympathy with the
	// container count. An app whose drive is absent is never recreated here, whatever its intent —
	// the drive gate's Return branch owns it. This is the exact term the BOOT SWEEP was missing
	// (R-171), so removing it here would reproduce that hazard in the other gate.
	absent := map[string]bool{rgDrive: false}
	for _, desired := range []string{stacks.DesiredStateRunning, stacks.DesiredStateStopped, stacks.DesiredStateUnknown} {
		if shouldRecreateOnBoot(true, rgDrive, absent, true, desired) {
			t.Fatalf("an app whose drive is ABSENT was recreated (desired=%q) — the drive-presence "+
				"term is load-bearing and must outrank intent", desired)
		}
	}
}

func TestShouldRecreateOnBoot_OtherGuardsSurviveTheRewrite(t *testing.T) {
	// deployed / stable-parent-prefix / HDD_PATH were terms before R-170 and must still be, at the
	// strongest intent available — the rewrite reordered the terms and a reorder is how a guard
	// silently disappears.
	if shouldRecreateOnBoot(false, rgDrive, rgPresent(), true, stacks.DesiredStateRunning) {
		t.Fatal("a NOT-deployed app was recreated")
	}
	if shouldRecreateOnBoot(true, "", rgPresent(), true, stacks.DesiredStateRunning) {
		t.Fatal("an app with no HDD_PATH (SSD-resident) was recreated by the DRIVE gate")
	}
	sys := "/mnt/sys_drive/felhom-data"
	if shouldRecreateOnBoot(true, sys, map[string]bool{sys: true}, true, stacks.DesiredStateRunning) {
		t.Fatal("a non-stable-parent (system) path was recreated by the drive gate")
	}
}

// --- Scenario F — the two boot gates agree --------------------------------------------------------

func TestShouldRecreateOnBoot_AgreesWithBootrecon(t *testing.T) {
	// The sibling of bootrecon's TestBothBootGatesAgreeOnIntent, over the SAME fixture table. The two
	// gates cannot be called from one package without an import cycle, so the agreement is pinned
	// from both sides against an identical table. If this table changes, change the other.
	//
	// Both answer: "did the customer want this app running?" On a live drive, this gate's verdict
	// must equal that answer exactly.
	cases := []struct {
		desired       string
		hasContainers bool
		wantWanted    bool
	}{
		{stacks.DesiredStateStopped, false, false},
		{stacks.DesiredStateStopped, true, false},
		{stacks.DesiredStateRunning, false, true},
		{stacks.DesiredStateRunning, true, true},
		{stacks.DesiredStateUnknown, false, false},
		{stacks.DesiredStateUnknown, true, true},
	}
	for _, c := range cases {
		got := shouldRecreateOnBoot(true, rgDrive, rgPresent(), c.hasContainers, c.desired)
		if got != c.wantWanted {
			t.Fatalf("gate disagreement: shouldRecreateOnBoot(desired=%q containers=%v) = %v, but "+
				"bootrecon.isBootOrphan answers %v for the same facts. Two boot gates answering the "+
				"same question differently is how R-157 mechanism B survived a year",
				c.desired, c.hasContainers, got, c.wantWanted)
		}
	}
}

// --- the counter that reports the honoured path ---------------------------------------------------

func TestRecreateDriveBackedApps_RecordedStopCountsAsLeftStopped(t *testing.T) {
	// `leftStopped` has always meant "deliberately not touched", and it is reported at INFO as the
	// gate working as intended. After R-170 a recorded Stop must land in that bucket — NOT in
	// `skipped`, which is the "bind never went live" failure and fires a WARN.
	apps := []bootStack{
		{name: "stopped-app", deployed: true, hdd: rgDrive, hasContainers: false, desired: stacks.DesiredStateStopped},
		{name: "legacy-stopped", deployed: true, hdd: rgDrive, hasContainers: false, desired: stacks.DesiredStateUnknown},
		{name: "wanted-running", deployed: true, hdd: rgDrive, hasContainers: false, desired: stacks.DesiredStateRunning},
	}
	var recreated []string
	n, skipped, leftStopped := recreateDriveBackedApps(apps, rgPresent(),
		func(bs bootStack) { recreated = append(recreated, bs.name) }, func() {})

	if n != 1 || len(recreated) != 1 || recreated[0] != "wanted-running" {
		t.Fatalf("recreated=%v (n=%d), want exactly [wanted-running] — the zero-container app the "+
			"customer wants RUNNING is the R-170 case", recreated, n)
	}
	if leftStopped != 2 {
		t.Fatalf("leftStopped=%d, want 2 (the recorded Stop and the legacy zero-container app)", leftStopped)
	}
	if skipped != 0 {
		t.Fatalf("skipped=%d, want 0 — the drive is live, so nothing was skipped for a missing bind; "+
			"counting a deliberate Stop as skipped would fire a WARN for healthy behaviour", skipped)
	}
}
