package reconcile

import (
	"context"
	"encoding/json"
	"path/filepath"
	"testing"

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

// --- The disk-grow executor (deferred from slice 4): grow applies, shrink refused. ---

func TestPlan_DiskGrowOnly(t *testing.T) {
	norm := DefaultNormalizers()

	t.Run("grow emits a resize action", func(t *testing.T) {
		desired := DesiredState{Guests: map[int]DesiredGuest{
			100: {VMID: 100, Spec: &hub.GuestSpec{DiskBytes: 20 << 30}}, // want 20 GiB
		}}
		actual := ActualState{Guests: map[int]ActualGuest{
			100: {VMID: 100, SpecKnown: true, DiskBytes: 8 << 30}, // have 8 GiB
		}}
		var resize *Action
		for _, a := range Plan(desired, actual, norm) {
			if a.Kind == ActionResize {
				a := a
				resize = &a
			}
		}
		if resize == nil {
			t.Fatal("expected a resize action for a grow")
		}
		if resize.Params["disk"] != "rootfs" || resize.Params["size"] != "+12288M" {
			t.Errorf("resize params = %v, want disk=rootfs size=+12288M", resize.Params)
		}
	})

	t.Run("shrink is refused (no action)", func(t *testing.T) {
		desired := DesiredState{Guests: map[int]DesiredGuest{100: {VMID: 100, Spec: &hub.GuestSpec{DiskBytes: 4 << 30}}}}
		actual := ActualState{Guests: map[int]ActualGuest{100: {VMID: 100, SpecKnown: true, DiskBytes: 8 << 30}}}
		for _, a := range Plan(desired, actual, norm) {
			if a.Kind == ActionResize {
				t.Fatalf("a data-losing shrink must NOT be planned as a resize: %+v", a)
			}
		}
	})

	t.Run("equal size is a no-op", func(t *testing.T) {
		desired := DesiredState{Guests: map[int]DesiredGuest{100: {VMID: 100, Spec: &hub.GuestSpec{DiskBytes: 8 << 30}}}}
		actual := ActualState{Guests: map[int]ActualGuest{100: {VMID: 100, SpecKnown: true, DiskBytes: 8 << 30}}}
		for _, a := range Plan(desired, actual, norm) {
			if a.Kind == ActionResize {
				t.Fatalf("equal disk size must not resize: %+v", a)
			}
		}
	})
}

func TestEngine_GrowExecutes_NonGrowRefusedAtExecutor(t *testing.T) {
	a := &fakeAPI{resizeUPID: "UPID:resize:1"}
	e, _, q := newEngine(t, a, EmptyProvider{})
	defer q.Close()

	// A grow applies (ResizeLXC called with the grow size).
	if err := e.execute(context.Background(), Action{VMID: 100, Kind: ActionResize,
		Params: map[string]string{"disk": "rootfs", "size": "+12288M"}}); err != nil {
		t.Fatalf("grow execute: %v", err)
	}
	if len(a.resizes) != 1 || a.resizes[0].size != "+12288M" || a.resizes[0].disk != "rootfs" {
		t.Fatalf("ResizeLXC not called correctly: %+v", a.resizes)
	}

	// A non-grow ("absolute"/shrink) size is refused at the executor and never hits the API.
	a.resizes = nil
	if err := e.execute(context.Background(), Action{VMID: 100, Kind: ActionResize,
		Params: map[string]string{"disk": "rootfs", "size": "4G"}}); err == nil {
		t.Fatal("a non-grow resize size must be refused at the executor")
	}
	if len(a.resizes) != 0 {
		t.Fatalf("refused resize must not call the API: %+v", a.resizes)
	}
}

// --- Destructive storage ops through the slice-4 gate (real verifier). ---

func wipeIntent(targetID string) Intent {
	return IntentForStorageDestructive(ClassStorageWipe, testHost, targetID,
		json.RawMessage(`{"wipe":true}`), SourceOneShotJob)
}

func TestGate_StorageWipeUnsignedPendingSignature(t *testing.T) {
	op := newTestSigner(t)
	v, _ := realVerifierAt(t, filepath.Join(t.TempDir(), "n.log"), testHost, op.allowed(t, "op1", authz.RoleOperational))
	aud := &captureAudit{}
	g := NewGate(v, testHost, aud, nil)

	d := g.Authorize(wipeIntent("usb-backup"), nil)
	if d.Allowed || d.Reason != ReasonPendingSignature {
		t.Fatalf("unsigned storage wipe: got allowed=%v reason=%s, want pending_signature", d.Allowed, d.Reason)
	}
	if len(aud.recs) != 1 || aud.recs[0].Allowed {
		t.Errorf("refused wipe must be audited: %+v", aud.recs)
	}
}

func TestGate_StorageWipeWrongTargetBindingMismatch(t *testing.T) {
	// A valid signature authorizing "wipe target A" must NOT authorize "wipe target B" —
	// the op-to-action binding rejects it (the storage analog of the wrong-guest case).
	op := newTestSigner(t)
	v, _ := realVerifierAt(t, filepath.Join(t.TempDir(), "n.log"), testHost, op.allowed(t, "op1", authz.RoleOperational))
	g := NewGate(v, testHost, nil, nil)

	issued, expires := freshWindow()
	signed := op.mint("storage_wipe", testHost, "usb-backup", "op1", nonce(), `{"wipe":true}`, issued, expires)
	d := g.Authorize(wipeIntent("nfs-arch"), signed) // action targets a DIFFERENT store
	if d.Allowed || d.Reason != ReasonBindingMismatch {
		t.Fatalf("wrong-target wipe: got allowed=%v reason=%s, want binding_mismatch", d.Allowed, d.Reason)
	}
}

func TestGate_StorageWipeValidAccepted(t *testing.T) {
	op := newTestSigner(t)
	v, _ := realVerifierAt(t, filepath.Join(t.TempDir(), "n.log"), testHost, op.allowed(t, "op1", authz.RoleOperational))
	g := NewGate(v, testHost, nil, nil)

	issued, expires := freshWindow()
	signed := op.mint("storage_wipe", testHost, "usb-backup", "op1", nonce(), `{"wipe":true}`, issued, expires)
	d := g.Authorize(wipeIntent("usb-backup"), signed)
	if !d.Allowed || d.Reason != ReasonSigned {
		t.Fatalf("valid storage wipe: got allowed=%v reason=%s err=%v, want accepted/signed", d.Allowed, d.Reason, d.Err)
	}
}

func TestGate_StorageMountBenign(t *testing.T) {
	// A re-mount is benign by classification → allowed unsigned (no verifier needed).
	g := NewGate(nil, testHost, nil, nil)
	d := g.Authorize(IntentForStorageMount(testHost, "usb-backup"), nil)
	if !d.Allowed || d.Reason != ReasonBenign {
		t.Fatalf("storage mount: got allowed=%v reason=%s, want benign", d.Allowed, d.Reason)
	}
}

// TestRunSignedJob_StorageDestructiveExecutes proves the authorized destructive-storage path
// reaches its executor (inert live — wired in main.go later; here a fake exec records it).
func TestRunSignedJob_StorageDestructiveExecutes(t *testing.T) {
	op := newTestSigner(t)
	v, _ := realVerifierAt(t, filepath.Join(t.TempDir(), "n.log"), testHost, op.allowed(t, "op1", authz.RoleOperational))
	a := &fakeAPI{}
	q := NewQueue()
	t.Cleanup(q.Close)
	e := NewEngine(EngineOptions{API: a, Queue: q, Gate: NewGate(v, testHost, nil, nil), HostID: testHost})

	issued, expires := freshWindow()
	signed := op.mint("storage_wipe", testHost, "usb-backup", "op1", nonce(), `{"wipe":true}`, issued, expires)

	var ran bool
	exec := func(_ context.Context, intent Intent, _ *authz.VerifiedOp) (string, error) {
		ran = true
		if intent.GuestID != "usb-backup" {
			t.Errorf("executor got wrong target %q", intent.GuestID)
		}
		return "", nil
	}
	res := e.RunSignedJob(context.Background(), wipeIntent("usb-backup"), signed, exec)
	if !res.Executed || !ran || res.Err != nil {
		t.Fatalf("authorized storage wipe should execute cleanly: %+v ran=%v", res, ran)
	}
}
