package web

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)

// addNetworkPath registers a Kind=network StoragePath in the test server's settings.
func addNetworkPath(t *testing.T, s *Server, name string) {
	t.Helper()
	if err := s.settings.AddStoragePath(settings.StoragePath{
		Path:        settings.NetworkMountRoot + "/" + name,
		Label:       "NAS " + name,
		Schedulable: true,
		AddedAt:     time.Now().UTC().Format(time.RFC3339),
		Kind:        settings.StorageKindNetwork,
		Protocol:    "nfs", Server: "10.0.0.5", Export: "/srv/" + name,
		MappedUID: 1000, MappedGID: 1000,
	}); err != nil {
		t.Fatal(err)
	}
}

func decodeEnvelope(t *testing.T, w *httptest.ResponseRecorder) map[string]any {
	t.Helper()
	var m map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &m); err != nil {
		t.Fatalf("decode: %v (%s)", err, w.Body.String())
	}
	return m
}

// TestNetStorage_RegisteredAndListed: a Kind=network path is registered and surfaced by the list handler
// (with health "unknown" when the agent is not reachable in the test).
func TestNetStorage_RegisteredAndListed(t *testing.T) {
	s := testServer(t)
	addNetworkPath(t, s, "media")

	r := httptest.NewRequest(http.MethodGet, "/api/storage/netstorage", nil)
	w := httptest.NewRecorder()
	s.handleNetStorageList(w, r)
	if w.Code != http.StatusOK {
		t.Fatalf("list: got %d want 200", w.Code)
	}
	body := w.Body.String()
	if !strings.Contains(body, `"name":"media"`) || !strings.Contains(body, `"protocol":"nfs"`) {
		t.Fatalf("network share not listed: %s", body)
	}
	if !strings.Contains(body, `"health":"unknown"`) {
		t.Fatalf("expected unknown health with no agent, got: %s", body)
	}
}

// TestNetStorage_KindGate is the §10 companion: the drive-lifecycle handlers REFUSE a Kind=network path.
// A network path → 400 refusal (the Kind-gate short-circuits BEFORE any agent call). The companion
// red-proof: a DRIVE path (or an unknown path) is NOT refused by the gate — it proceeds and fails later
// with a 503 (no agent configured in the test). A Kind-blind impl would treat both alike and FAIL here.
func TestNetStorage_KindGate(t *testing.T) {
	s := testServer(t)
	addNetworkPath(t, s, "media")

	lifecycle := []struct {
		name        string
		path        string
		handler     func(http.ResponseWriter, *http.Request)
		driveCompan bool // a drive path fails cleanly at the agent step (no stackMgr needed) → safe to assert
	}{
		{"eject", "/api/storage/eject", s.handleStorageEject, true},
		{"wipe", "/api/storage/wipe", s.handleStorageWipe, true},
		{"decommission", "/api/storage/decommission", s.handleStorageDecommission, false},
	}
	for _, lc := range lifecycle {
		// A NETWORK path is refused by the Kind-gate (400, before any agent/stackMgr use).
		netBody := `{"where":"/mnt/felhom-drives/media","device":"/dev/sdz","mount_name":"media","mode":"anyway"}`
		rn := httptest.NewRequest(http.MethodPost, lc.path, strings.NewReader(netBody))
		wn := httptest.NewRecorder()
		lc.handler(wn, rn)
		if wn.Code != http.StatusBadRequest {
			t.Fatalf("%s on a network path: got %d want 400 (Kind-gate must refuse)", lc.name, wn.Code)
		}
		if env := decodeEnvelope(t, wn); env["ok"] != false {
			t.Fatalf("%s on a network path must be ok:false, got %v", lc.name, env["ok"])
		}

		// COMPANION: a DRIVE path is NOT refused by the Kind-gate — it falls through to the agent step and
		// fails 503 (no agent), proving the gate keys on Kind, not the path. (Only for handlers that reach
		// the agent before any nil-dependency; decommission-anyway dives into stackMgr, covered by the
		// direct refuseNetworkLifecycle unit test instead.)
		if !lc.driveCompan {
			continue
		}
		drvBody := `{"where":"/mnt/hdd_1","device":"/dev/sdz","mount_name":"hdd_1","mode":"anyway"}`
		rd := httptest.NewRequest(http.MethodPost, lc.path, strings.NewReader(drvBody))
		wd := httptest.NewRecorder()
		lc.handler(wd, rd)
		if wd.Code == http.StatusBadRequest {
			t.Fatalf("%s on a DRIVE path must NOT hit the Kind-gate 400 (got 400) — the gate would be Kind-blind", lc.name)
		}
	}
}

// TestNetStorage_UnreachableNotMissing is the §10 health-surface companion: an `unreachable` NAS must
// surface as a recoverable WARNING, never the drive "missing → stopped" cascade. Here: (1) a network path
// is NEVER reported "missing" by missingStorageLabel, and (2) the drive-absent gate produces NO Stop
// action for a network path even though it is absent from the agent's /disks list. The companion shows a
// real DRIVE path under the same parent, absent from /disks, DOES get a Stop action — proving the network
// exemption (not luck) is what prevents the cascade. An impl that mapped unreachable→missing FAILS (1).
func TestNetStorage_UnreachableNotMissing(t *testing.T) {
	s := testServer(t)
	addNetworkPath(t, s, "media") // registered network path /mnt/felhom-drives/media

	// (1) a network path is never "missing" (no stop-cascade label), regardless of agent reachability.
	if lbl, missing := s.missingStorageLabel("/mnt/felhom-drives/media"); missing {
		t.Fatalf("a network path must NOT be 'missing' (got missing=true, label=%q)", lbl)
	}

	// (2) the drive-absent gate skips the network path: with the agent reporting NO disks (the NAS is not
	// a drive — Scenario D), the network path is absent from /disks but must produce NO Stop action.
	netPaths := s.settings.GetStoragePaths()
	if acts := planDriveGates(netPaths, nil); len(acts) != 0 {
		t.Fatalf("the drive gate must produce NO action for a network path, got %+v", acts)
	}

	// COMPANION: a real DRIVE path under the same parent, absent from /disks, DOES get a Stop action —
	// proving the gate WOULD have stopped the network path's apps if it weren't exempted.
	drivePaths := []settings.StoragePath{{Path: StableParentDir + "/realhdd", Schedulable: true}}
	acts := planDriveGates(drivePaths, nil)
	if len(acts) != 1 || !acts[0].Stop {
		t.Fatalf("control: an absent DRIVE path under the parent must get a Stop action, got %+v", acts)
	}
}

// TestRefuseNetworkLifecycle is the unit-level discriminator: network → refused, drive/unknown → not.
func TestRefuseNetworkLifecycle(t *testing.T) {
	s := testServer(t)
	addNetworkPath(t, s, "media")

	w1 := httptest.NewRecorder()
	if !s.refuseNetworkLifecycle(w1, "/mnt/felhom-drives/media") {
		t.Fatal("a network path must be refused")
	}
	w2 := httptest.NewRecorder()
	if s.refuseNetworkLifecycle(w2, "/mnt/hdd_1") {
		t.Fatal("a drive/unknown path must NOT be refused by the network gate")
	}
}
