package reconcile

import (
	"context"
	"testing"

	"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
	"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
)

// The headline slice-10A reconcile behaviour: a desired-state carrying ONE benign delta and ONE
// destructive (decommission) delta → the benign op is applied, the destructive one is GATED
// pending_signature (counted Pending, NOT Failed) and is NEVER executed (no signer in 10A).
func TestReconcile_BenignAppliedDestructiveGated(t *testing.T) {
	api := &fakeAPI{
		lxc: []proxmox.Guest{
			{VMID: 100, Status: "stopped"}, // benign: desired running → Start
			{VMID: 200, Status: "running"}, // destructive: decommission → gated
		},
		cfg: map[int]proxmox.GuestConfig{100: {Cores: 2}, 200: {Cores: 2}},
	}
	provider := StaticProvider{State: DesiredState{Guests: map[int]DesiredGuest{
		100: {VMID: 100, Run: RunRunning},
		200: {VMID: 200, Decommission: true},
	}}}
	e, _, _ := newEngine(t, api, provider)

	res, err := e.Reconcile(context.Background())
	if err != nil {
		t.Fatalf("Reconcile: %v", err)
	}
	if res.Planned != 2 {
		t.Errorf("planned = %d, want 2 (one benign + one destructive)", res.Planned)
	}
	// Benign Start(100) applied.
	if res.Executed != 1 || len(api.starts) != 1 || api.starts[0] != 100 {
		t.Errorf("benign delta not applied: executed=%d starts=%v", res.Executed, api.starts)
	}
	// Destructive decommission GATED pending (not a failure).
	if res.Pending != 1 {
		t.Errorf("pending = %d, want 1 (decommission gated pending_signature)", res.Pending)
	}
	if res.Failed != 0 {
		t.Errorf("failed = %d, want 0 (a pending_signature gate is expected, not a failure)", res.Failed)
	}
	// And it was NEVER executed: no destroy/decommission op reached Proxmox.
	if len(api.destroys) != 0 {
		t.Errorf("destructive decommission EXECUTED (destroys=%v) — it must be gated, not run", api.destroys)
	}
}

// Plan unit: an explicit Decommission emits exactly one ActionDecommission and suppresses any
// other delta for that guest (no point reconciling cores on a guest being torn down).
func TestPlan_DecommissionEmitsDestructiveActionOnly(t *testing.T) {
	desired := DesiredState{Guests: map[int]DesiredGuest{
		// Decommission set AND a spec drift — only the decommission should be emitted.
		7: {VMID: 7, Decommission: true, Spec: &hub.GuestSpec{Cores: 9, MemoryBytes: 9 << 20}, Run: RunStopped},
	}}
	actual := ActualState{Guests: map[int]ActualGuest{
		7: {VMID: 7, Run: RunRunning, SpecKnown: true, Cores: 2},
	}}
	actions := Plan(desired, actual, DefaultNormalizers())
	if len(actions) != 1 {
		t.Fatalf("actions = %d (%+v), want exactly 1 (decommission only)", len(actions), actions)
	}
	if actions[0].Kind != ActionDecommission || actions[0].VMID != 7 {
		t.Errorf("action = %+v, want decommission of vmid 7", actions[0])
	}
	// And it classifies destructive.
	if Classify(classOfAction(ActionDecommission), Provenance{}) != Destructive {
		t.Error("ActionDecommission must classify Destructive (no provenance)")
	}
}

// Decommission of an ABSENT guest is a no-op (nothing to tear down).
func TestPlan_DecommissionAbsentGuestNoop(t *testing.T) {
	desired := DesiredState{Guests: map[int]DesiredGuest{7: {VMID: 7, Decommission: true}}}
	actual := ActualState{Guests: map[int]ActualGuest{}} // guest 7 not present
	if actions := Plan(desired, actual, DefaultNormalizers()); len(actions) != 0 {
		t.Errorf("decommission of absent guest emitted %+v, want none", actions)
	}
}

// CachingProvider: empty until Update, then serves the cached state + generation, and isolates
// the cache from caller mutation.
func TestCachingProvider_UpdateAndIsolation(t *testing.T) {
	p := NewCachingProvider()
	if p.Generation() != 0 {
		t.Fatalf("fresh generation = %d, want 0", p.Generation())
	}
	if st, _ := p.Desired(context.Background()); len(st.Guests) != 0 {
		t.Fatalf("fresh provider should be empty, got %+v", st.Guests)
	}

	p.Update(3, DesiredState{Guests: map[int]DesiredGuest{5: {VMID: 5, Run: RunRunning}}})
	if p.Generation() != 3 {
		t.Errorf("generation after update = %d, want 3", p.Generation())
	}
	st, _ := p.Desired(context.Background())
	if len(st.Guests) != 1 || st.Guests[5].Run != RunRunning {
		t.Fatalf("cached state = %+v", st.Guests)
	}
	// Mutating the returned copy must NOT affect the cache.
	st.Guests[5] = DesiredGuest{VMID: 5, Run: RunStopped}
	st.Guests[99] = DesiredGuest{VMID: 99}
	st2, _ := p.Desired(context.Background())
	if len(st2.Guests) != 1 || st2.Guests[5].Run != RunRunning {
		t.Errorf("cache was mutated by a caller: %+v", st2.Guests)
	}
}
