package reconcile

import (
	"context"
	"errors"
	"testing"
	"time"

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

func seedInFlight(t *testing.T, j *Journal, e JournalEntry) {
	t.Helper()
	e.State = OpTaskRunning
	if e.At.IsZero() {
		e.At = time.Now().UTC()
	}
	if err := j.Append(e); err != nil {
		t.Fatalf("seed: %v", err)
	}
}

func TestRecover_TaskCompletedOKMarksSucceeded(t *testing.T) {
	api := &fakeAPI{statusFunc: func(string) (proxmox.TaskStatus, error) {
		return proxmox.TaskStatus{Status: "stopped", ExitStatus: "OK"}, nil
	}}
	e, j, _ := newEngine(t, api, EmptyProvider{})
	seedInFlight(t, j, JournalEntry{OpID: "op1", VMID: 100, Kind: "set_config", UPID: "UPID:x:", IdempKey: "k1"})

	res := e.Recover(context.Background())
	if res.Examined != 1 || res.Resumed != 1 {
		t.Fatalf("want 1 resumed, got %+v", res)
	}
	if len(j.InFlight()) != 0 {
		t.Errorf("resolved op should not be in-flight: %+v", j.InFlight())
	}
	// A resumed one-shot op marks its idempotency key applied (it really completed) —
	// this is the case idempotency-alone could not cover (Note 1).
	if !j.AlreadyApplied("k1") {
		t.Error("a recovered-succeeded op must mark its idempotency key applied")
	}
}

func TestRecover_TaskEndedNonOKMarksFailed(t *testing.T) {
	api := &fakeAPI{statusFunc: func(string) (proxmox.TaskStatus, error) {
		return proxmox.TaskStatus{Status: "stopped", ExitStatus: "got 403"}, nil
	}}
	e, j, _ := newEngine(t, api, EmptyProvider{})
	seedInFlight(t, j, JournalEntry{OpID: "op2", VMID: 100, Kind: "guest_destroy", UPID: "UPID:x:", IdempKey: "k2"})

	res := e.Recover(context.Background())
	if res.Failed != 1 {
		t.Fatalf("want 1 failed, got %+v", res)
	}
	if j.AlreadyApplied("k2") {
		t.Error("a failed op must NOT mark its key applied (it may be retried)")
	}
}

func TestRecover_TaskStillRunningLeftInFlight(t *testing.T) {
	api := &fakeAPI{statusFunc: func(string) (proxmox.TaskStatus, error) {
		return proxmox.TaskStatus{Status: "running"}, nil
	}}
	e, j, _ := newEngine(t, api, EmptyProvider{})
	seedInFlight(t, j, JournalEntry{OpID: "op3", VMID: 100, Kind: "set_config", UPID: "UPID:x:"})

	res := e.Recover(context.Background())
	if res.StillRunning != 1 || len(j.InFlight()) != 1 {
		t.Fatalf("still-running task must be left in-flight, got res=%+v inflight=%d", res, len(j.InFlight()))
	}
}

func TestRecover_NoTaskIDRolledBack(t *testing.T) {
	// OpStarted with no UPID: the POST was never confirmed → abandon (fail-safe).
	e, j, _ := newEngine(t, &fakeAPI{}, EmptyProvider{})
	if err := j.Append(JournalEntry{OpID: "op4", VMID: 100, Kind: "start", State: OpStarted, At: time.Now().UTC()}); err != nil {
		t.Fatal(err)
	}
	res := e.Recover(context.Background())
	if res.RolledBack != 1 || len(j.InFlight()) != 0 {
		t.Fatalf("no-task op must be rolled back, got res=%+v inflight=%d", res, len(j.InFlight()))
	}
}

func TestRecover_UnreadableStatusLeftInFlight(t *testing.T) {
	api := &fakeAPI{statusFunc: func(string) (proxmox.TaskStatus, error) {
		return proxmox.TaskStatus{}, errors.New("api unreachable")
	}}
	e, j, _ := newEngine(t, api, EmptyProvider{})
	seedInFlight(t, j, JournalEntry{OpID: "op5", VMID: 100, Kind: "set_config", UPID: "UPID:x:"})

	res := e.Recover(context.Background())
	if res.Unresolved != 1 || len(j.InFlight()) != 1 {
		t.Fatalf("unreadable status must leave op in-flight, got res=%+v inflight=%d", res, len(j.InFlight()))
	}
}

func TestRecover_EmptyJournalNoop(t *testing.T) {
	e, _, _ := newEngine(t, &fakeAPI{}, EmptyProvider{})
	if res := e.Recover(context.Background()); res.Examined != 0 {
		t.Errorf("empty journal recover should be a no-op, got %+v", res)
	}
}
