package localapi

import (
	"context"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"path/filepath"
	"testing"
	"time"

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

func tempFormatStore(t *testing.T) *FormatJobStore {
	t.Helper()
	fj, err := OpenFormatJobStore(filepath.Join(t.TempDir(), "format-job.json"))
	if err != nil {
		t.Fatal(err)
	}
	return fj
}

// formatServer builds a *Server with the destructive-format path wired (data-bearing probe, a gate, the
// format-job store), plus the test stubs (reresolveWipe/deviceDurableID) so no real /dev is touched.
func formatServer(t *testing.T, d *fakeDiskOps, g StorageGate, fj *FormatJobStore) *Server {
	t.Helper()
	srv, err := NewServer(Options{
		ListenAddr: "127.0.0.1:0", Guests: &fakeGuests{}, Backups: &fakeBackups{}, Store: &fakeStore{},
		Storage: fakeStorage{}, Tokens: staticTokens{"A": 8200},
		Disks: d, DiskGate: g, FormatJobs: fj, HostReader: sysOnSDA(),
		Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
	})
	if err != nil {
		t.Fatal(err)
	}
	srv.baseCtx = context.Background()
	srv.reresolveWipe = func(_ context.Context, _ string) (string, error) { return "/dev/sdb", nil }
	srv.deviceDurableID = func(device string) (string, error) { return "byid:wwn-" + device, nil }
	return srv
}

func waitFormatPhase(t *testing.T, fj *FormatJobStore, want string) *formatJob {
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if j := fj.get(); j != nil && j.Phase == want {
			return j
		}
		time.Sleep(10 * time.Millisecond)
	}
	got := fj.get()
	t.Fatalf("format job did not reach phase %q in time (got %+v)", want, got)
	return nil
}

// TestFormat_DetachedPersistsJobRecord asserts F20-BUG3: a customer-confirmed wipe runs through the
// detached runner and a persisted job record reaches `done` with the device + durable-id bound (so it
// can be polled / recovered). Also confirms backward-compat: the handler still returns 200 synchronously.
func TestFormat_DetachedPersistsJobRecord(t *testing.T) {
	d := &fakeDiskOps{probe: deviceProbeDataBearing()}
	g := &fakeGate{decision: WipeDecision{Allowed: true, Tier: "customer_confirmable", Reason: "customer_confirmed"}}
	fj := tempFormatStore(t)
	srv := formatServer(t, d, g, fj)
	h := srv.Handler()

	w := do(t, h, "POST", "/disks/format", "A", `{"device":"/dev/sdb1","fstype":"ext4","confirmed":true,"durable_id":"byid:wwn-/dev/sdb1"}`)
	if w.Code != http.StatusOK {
		t.Fatalf("confirmed format: %d (%s)", w.Code, w.Body.String())
	}
	job := waitFormatPhase(t, fj, formatPhaseDone)
	if job.Device != "/dev/sdb" { // the anti-retarget re-resolved device (stub returns /dev/sdb)
		t.Fatalf("job device = %q, want /dev/sdb (re-resolved)", job.Device)
	}
	if job.DurableID == "" {
		t.Fatalf("job durable-id empty — recovery could not re-bind")
	}
	if len(d.formatted()) != 1 {
		t.Fatalf("mkfs called %d times, want 1", len(d.formatted()))
	}
}

// TestFormatStatus reports the persisted job via GET /disks/format/status.
func TestFormatStatus(t *testing.T) {
	d := &fakeDiskOps{probe: deviceProbeDataBearing()}
	g := &fakeGate{decision: WipeDecision{Allowed: true, Tier: "customer_confirmable", Reason: "customer_confirmed"}}
	fj := tempFormatStore(t)
	srv := formatServer(t, d, g, fj)
	h := srv.Handler()

	do(t, h, "POST", "/disks/format", "A", `{"device":"/dev/sdb1","fstype":"ext4","confirmed":true,"durable_id":"byid:wwn-/dev/sdb1"}`)
	waitFormatPhase(t, fj, formatPhaseDone)

	w := do(t, h, "GET", "/disks/format/status", "A", "")
	if w.Code != http.StatusOK {
		t.Fatalf("status: %d", w.Code)
	}
	var resp struct {
		Data struct {
			Phase  string `json:"phase"`
			Device string `json:"device"`
		} `json:"data"`
	}
	_ = json.Unmarshal(w.Body.Bytes(), &resp)
	if resp.Data.Phase != formatPhaseDone {
		t.Fatalf("status phase = %q, want done", resp.Data.Phase)
	}
}

// TestRecoverFormatJob_ReRunsInterrupted asserts the operator's "survive an agent restart" decision: a
// record left in `running` (agent died mid-format) is re-resolved by durable-id and the mkfs re-run.
func TestRecoverFormatJob_ReRunsInterrupted(t *testing.T) {
	d := &fakeDiskOps{probe: deviceProbeDataBearing()}
	fj := tempFormatStore(t)
	// Pre-seed an interrupted, durable-id-bound running job (as if the agent died mid-mkfs).
	if err := fj.save(&formatJob{JobID: "j1", Device: "/dev/sdb", DurableID: "byid:wwn-x", FSType: "ext4", Phase: formatPhaseRunning}); err != nil {
		t.Fatal(err)
	}
	srv := formatServer(t, d, &fakeGate{}, fj)

	srv.RecoverFormatJob(context.Background())
	waitFormatPhase(t, fj, formatPhaseDone)
	if got := d.formatted(); len(got) != 1 || got[0] != "/dev/sdb" {
		t.Fatalf("recovery should have re-run mkfs on /dev/sdb (re-resolved), got %v", got)
	}
}

// TestRecoverFormatJob_SkipsBlank: an interrupted BLANK (no durable-id) format is NOT auto-re-run (never
// format a mutable /dev path on recovery) — it is marked failed for the caller to retry.
func TestRecoverFormatJob_SkipsBlank(t *testing.T) {
	d := &fakeDiskOps{probe: deviceProbeDataBearing()}
	fj := tempFormatStore(t)
	_ = fj.save(&formatJob{JobID: "j2", Device: "/dev/sdb", DurableID: "", FSType: "ext4", Phase: formatPhaseRunning})
	srv := formatServer(t, d, &fakeGate{}, fj)

	srv.RecoverFormatJob(context.Background())
	if got := d.formatted(); len(got) != 0 {
		t.Fatalf("recovery must NOT re-run a path-bound (blank) format, but mkfs ran: %v", got)
	}
	if j := fj.get(); j == nil || j.Phase != formatPhaseFailed {
		t.Fatalf("blank interrupted job should be marked failed, got %+v", j)
	}
}

// TestRecoverFormatJob_SkipsUnresolvable: if the durable-id no longer resolves (drive removed/replaced),
// recovery must NOT format anything (anti-retarget) and marks the job failed.
func TestRecoverFormatJob_SkipsUnresolvable(t *testing.T) {
	d := &fakeDiskOps{probe: deviceProbeDataBearing()}
	fj := tempFormatStore(t)
	_ = fj.save(&formatJob{JobID: "j3", Device: "/dev/sdb", DurableID: "byid:wwn-gone", FSType: "ext4", Phase: formatPhaseRunning})
	srv := formatServer(t, d, &fakeGate{}, fj)
	srv.reresolveWipe = func(_ context.Context, _ string) (string, error) {
		return "", context.DeadlineExceeded // simulate "durable-id no longer resolves"
	}

	srv.RecoverFormatJob(context.Background())
	if got := d.formatted(); len(got) != 0 {
		t.Fatalf("recovery must NOT format when the durable-id is unresolvable: %v", got)
	}
	if j := fj.get(); j == nil || j.Phase != formatPhaseFailed {
		t.Fatalf("unresolvable interrupted job should be marked failed, got %+v", j)
	}
}

func deviceProbeDataBearing() storage.DeviceProbe {
	return storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"}
}
