package api

import (
	"go/ast"
	"go/parser"
	"go/token"
	"testing"

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

// R-166 Part 1.3 — THE CUSTOMER-INTENT POINT.
//
// `stackMgr` is a concrete *stacks.Manager, so actionStack cannot be driven with a fake without
// Docker. The two properties that actually carry the correctness are therefore pinned the only way
// they can be: the mapping is a pure function with its own table test, and the ORDER (§8.2) is
// asserted structurally over actionStack's AST. Both fail if someone reverses the write and the act,
// which is the mistake that would undo a customer's Stop at the next boot.

func TestDesiredStateForAction_MapsEveryAction(t *testing.T) {
	cases := []struct {
		action string
		want   string
		ok     bool
	}{
		{"start", stacks.DesiredStateRunning, true},
		// restart and update both END in `compose up -d`, so a customer who presses either is asking
		// for the app to be up afterwards.
		{"restart", stacks.DesiredStateRunning, true},
		{"update", stacks.DesiredStateRunning, true},
		{"stop", stacks.DesiredStateStopped, true},
		// Anything unrecognised records NOTHING rather than guessing — a future action must not
		// silently acquire an intent it was never meant to carry.
		{"", "", false},
		{"delete", "", false},
		{"pause", "", false},
	}
	for _, tc := range cases {
		got, ok := desiredStateForAction(tc.action)
		if got != tc.want || ok != tc.ok {
			t.Fatalf("desiredStateForAction(%q) = (%q, %v), want (%q, %v)", tc.action, got, ok, tc.want, tc.ok)
		}
	}
}

func TestDesiredStateForAction_NeverRecordsStoppedForANonStop(t *testing.T) {
	// The asymmetry that matters: writing "stopped" for anything other than a Stop would permanently
	// disable auto-recovery for an app nobody stopped.
	for _, a := range []string{"start", "restart", "update", "deploy", "delete", ""} {
		if got, _ := desiredStateForAction(a); got == stacks.DesiredStateStopped {
			t.Fatalf("action %q maps to desired_state=stopped", a)
		}
	}
}

// TestActionStack_RecordsIntentBeforeActing is §8.2, asserted structurally.
//
// If the SetDesiredState call moved BELOW the action switch, a stop could remove every container
// while app.yaml still recorded `running` — and the boot reconciler would then start an app the
// customer had just deliberately stopped. That is the single worst outcome available in Part 1, and
// no behavioural test in this package can reach it without a Docker daemon.
func TestActionStack_RecordsIntentBeforeActing(t *testing.T) {
	body := funcBody(t, "actionStack")

	setPos, switchPos := -1, -1
	ast.Inspect(body, func(n ast.Node) bool {
		switch node := n.(type) {
		case *ast.CallExpr:
			if sel, ok := node.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "SetDesiredState" && setPos < 0 {
				setPos = int(node.Pos())
			}
		case *ast.SwitchStmt:
			// The action switch is the one whose tag is the `action` identifier.
			if id, ok := node.Tag.(*ast.Ident); ok && id.Name == "action" && switchPos < 0 {
				switchPos = int(node.Pos())
			}
		}
		return true
	})

	if setPos < 0 {
		t.Fatal("actionStack no longer calls SetDesiredState — the customer's start/stop decision is " +
			"recorded nowhere, which is the R-166 defect un-fixed")
	}
	if switchPos < 0 {
		t.Fatal("actionStack no longer has a `switch action` — this test needs updating")
	}
	if setPos >= switchPos {
		t.Fatal("actionStack records the desired state AFTER performing the action (§8.2 violated): a " +
			"stop whose intent write fails or lands late leaves zero containers with `running` " +
			"recorded, and the boot reconciler would restart an app the customer just stopped")
	}
}

// TestActionStack_RefusesTheActionWhenIntentCannotBeRecorded pins the other half of §8.2: a failed
// write REFUSES the act. Proceeding anyway would perform a stop that nothing records — exactly the
// ambiguity this release removes.
func TestActionStack_RefusesTheActionWhenIntentCannotBeRecorded(t *testing.T) {
	body := funcBody(t, "actionStack")

	refuses := false
	ast.Inspect(body, func(n ast.Node) bool {
		ifst, ok := n.(*ast.IfStmt)
		if !ok || ifst.Init == nil {
			return true
		}
		// Look for `if derr := ...SetDesiredState(...); derr != nil { ... return }`
		assign, ok := ifst.Init.(*ast.AssignStmt)
		if !ok || len(assign.Rhs) != 1 {
			return true
		}
		call, ok := assign.Rhs[0].(*ast.CallExpr)
		if !ok {
			return true
		}
		sel, ok := call.Fun.(*ast.SelectorExpr)
		if !ok || sel.Sel.Name != "SetDesiredState" {
			return true
		}
		for _, stmt := range ifst.Body.List {
			if _, isReturn := stmt.(*ast.ReturnStmt); isReturn {
				refuses = true
			}
		}
		return true
	})

	if !refuses {
		t.Fatal("actionStack does not RETURN when SetDesiredState fails — it would go on to stop or " +
			"start an app whose intent could not be recorded (§8.2)")
	}
}

// funcBody parses router.go and returns the named method's body.
func funcBody(t *testing.T, name string) *ast.BlockStmt {
	t.Helper()
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "router.go", nil, 0)
	if err != nil {
		t.Fatalf("parse router.go: %v", err)
	}
	for _, decl := range f.Decls {
		if fn, ok := decl.(*ast.FuncDecl); ok && fn.Name.Name == name && fn.Body != nil {
			return fn.Body
		}
	}
	t.Fatalf("func %s not found in router.go", name)
	return nil
}
