package localapi

import (
	"encoding/json"
	"net/http"
	"testing"

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

// decodeDisks pulls the disks array out of a GET /disks response.
func decodeDisks(t *testing.T, body []byte) []DiskInfo {
	t.Helper()
	var resp struct {
		Data struct {
			Disks []DiskInfo `json:"disks"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		t.Fatalf("decode /disks: %v (body=%s)", err, string(body))
	}
	return resp.Data.Disks
}

// TestDisks_WipeDurableID_GateScheme asserts F20-BUG2: /disks surfaces a wipe_durable_id in the gate's
// scheme (byid:/byuuid:, via the SAME s.deviceDurableID seam the format gate uses), DISTINCT from the
// uuid: durable_id (which feeds /disks/assign). Pre-fix the only id was uuid:, which the gate rejected
// as a binding_mismatch.
func TestDisks_WipeDurableID_GateScheme(t *testing.T) {
	d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"}}
	sv := fakeStorage{targets: []hub.StorageTarget{
		{Name: "bulk", Type: hub.StorageTypeUSB, BackingDevice: "/dev/sdb1", MountPath: "/mnt/bulk", DurableID: "uuid:abc-123"},
	}}
	h := newDiskServer(t, d, &fakeGate{}, sv, nil)

	w := do(t, h, "GET", "/disks", "A", "")
	if w.Code != http.StatusOK {
		t.Fatalf("GET /disks: %d (%s)", w.Code, w.Body.String())
	}
	disks := decodeDisks(t, w.Body.Bytes())
	if len(disks) != 1 {
		t.Fatalf("want 1 disk, got %d", len(disks))
	}
	got := disks[0]
	// The stub in newDiskServer maps /dev/sdb1 → byid:wwn-sdb1 (the gate scheme).
	if got.WipeDurableID != "byid:wwn-sdb1" {
		t.Fatalf("WipeDurableID = %q, want byid:wwn-sdb1 (gate scheme)", got.WipeDurableID)
	}
	if got.DurableID != "uuid:abc-123" {
		t.Fatalf("DurableID = %q, want uuid:abc-123 (assign scheme, unchanged)", got.DurableID)
	}
	if got.WipeDurableID == got.DurableID {
		t.Fatal("wipe id must differ from the assign (uuid:) id")
	}
}

// TestDisks_WipeID_MatchesGateBinding asserts the BUG2 end-to-end property: when the customer confirms a
// wipe with the wipe_durable_id surfaced by /disks, the handler derives the device's gate id through the
// SAME seam — so the value the gate compares as DeviceDurableID equals the customer's ConfirmDurableID
// (no binding_mismatch). Asserted via the request the handler forwards to the gate.
func TestDisks_WipeID_MatchesGateBinding(t *testing.T) {
	d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"}}
	sv := fakeStorage{targets: []hub.StorageTarget{
		{Name: "bulk", Type: hub.StorageTypeUSB, BackingDevice: "/dev/sdb1", MountPath: "/mnt/bulk", DurableID: "uuid:abc-123"},
	}}
	g := &fakeGate{decision: WipeDecision{Allowed: true, Tier: "customer_confirmable", Reason: "customer_confirmed"}}
	h := newDiskServer(t, d, g, sv, nil)

	wipeID := decodeDisks(t, do(t, h, "GET", "/disks", "A", "").Body.Bytes())[0].WipeDurableID

	body := `{"device":"/dev/sdb1","fstype":"ext4","confirmed":true,"durable_id":"` + wipeID + `"}`
	w := do(t, h, "POST", "/disks/format", "A", body)
	if w.Code != http.StatusOK {
		t.Fatalf("confirmed wipe with the list's wipe id: %d (%s)", w.Code, w.Body.String())
	}
	reqs := g.requests()
	if len(reqs) != 1 {
		t.Fatalf("gate consulted %d times, want 1", len(reqs))
	}
	// The crux: the customer's confirm id (from the list) equals the device id the gate resolves.
	if reqs[0].ConfirmDurableID != wipeID {
		t.Fatalf("ConfirmDurableID forwarded = %q, want the list's wipe id %q", reqs[0].ConfirmDurableID, wipeID)
	}
	if reqs[0].DeviceDurableID != reqs[0].ConfirmDurableID {
		t.Fatalf("gate binding mismatch: device id %q != confirm id %q (BUG2 not fixed)", reqs[0].DeviceDurableID, reqs[0].ConfirmDurableID)
	}
}

// TestDisks_GuestAttached asserts F9 reporting: a drive bound into THIS guest's config (mp=<mount_path>)
// reports guest_attached=true; a host-present-but-unbound drive reports false — the signal that was
// missing when the HDD looked available but wasn't attached.
func TestDisks_GuestAttached(t *testing.T) {
	d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"}}
	sv := fakeStorage{targets: []hub.StorageTarget{
		{Name: "usb", Type: hub.StorageTypeUSB, BackingDevice: "/dev/sdb1", MountPath: "/mnt/felhom-usb"},
		{Name: "extra", Type: hub.StorageTypeUSB, BackingDevice: "/dev/sdc1", MountPath: "/mnt/extra"},
	}}
	// Guest 8200 (token "A") has /mnt/felhom-usb bound (Model-A bind: source .../felhom-data, mp=where),
	// but NOT /mnt/extra.
	h := newDiskServerWithGuestConfigs(t, d, sv, nil, map[int]map[string]string{
		8200: {"mp3": "/mnt/felhom-usb/felhom-data,mp=/mnt/felhom-usb"},
	})

	disks := decodeDisks(t, do(t, h, "GET", "/disks", "A", "").Body.Bytes())
	got := map[string]bool{}
	for _, di := range disks {
		got[di.MountPath] = di.GuestAttached
	}
	if !got["/mnt/felhom-usb"] {
		t.Errorf("/mnt/felhom-usb should be guest_attached=true (bound into the guest)")
	}
	if got["/mnt/extra"] {
		t.Errorf("/mnt/extra should be guest_attached=false (host-present but not bound)")
	}
}
