package web

// Scenarios C/D (hub v0.47.0 stale host removal) — the web-layer gates. Every refusal test
// asserts the NON-effect (the host and its artifacts still exist), not just the status code.
// The store-level cascade completeness lives in store/host_delete_test.go.

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

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

func postHostDelete(t *testing.T, s *Server, hostID string, form url.Values) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, "/hosts/"+hostID+"/delete", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	rr := httptest.NewRecorder()
	s.handleHostDelete(rr, req, hostID)
	return rr
}

// D1 — an ONLINE host is never deletable, even with a correct confirmation + escrow ack.
// RED-PROOF 1: removing the online gate in handleHostDelete makes this FAIL (host deleted).
func TestHostDelete_OnlineRefused(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.UpsertHost(&store.Host{HostID: "live-host", CustomerID: "c1", APIKey: "k"}); err != nil {
		t.Fatal(err)
	}
	// A just-saved report → status "ok" (online).
	if err := st.SaveHostReport("live-host", "c1", []byte(`{}`), store.HostReportDenorm{}); err != nil {
		t.Fatal(err)
	}
	before, err := st.CountHostArtifacts("live-host")
	if err != nil {
		t.Fatal(err)
	}

	rr := postHostDelete(t, s, "live-host", url.Values{
		"confirm_host_id": {"live-host"}, "delete_escrow": {"1"},
	})
	if rr.Code != http.StatusConflict {
		t.Fatalf("online delete = %d, want 409", rr.Code)
	}
	// Non-effect: the host row and every artifact are still there.
	if h, _ := st.GetHost("live-host"); h == nil {
		t.Fatal("online host was DELETED despite the 409")
	}
	after, _ := st.CountHostArtifacts("live-host")
	if after != before {
		t.Errorf("artifacts changed on a refused delete: %+v → %+v", before, after)
	}
}

// D2 — escrow present + no acknowledgement → 409 naming the escrow, ZERO deletions.
// RED-PROOF 2: dropping the escrow-ack check (passing deleteEscrow=true unconditionally)
// makes this FAIL (host + escrow deleted).
func TestHostDelete_EscrowAckRequired(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.UpsertHost(&store.Host{HostID: "esc-host", CustomerID: "c2", APIKey: "k"}); err != nil {
		t.Fatal(err)
	}
	if _, _, err := st.SaveHostEscrow("esc-host", []byte("blob"), "fp", "p", "2026-07-01T00:00:00Z", ""); err != nil {
		t.Fatal(err)
	}

	rr := postHostDelete(t, s, "esc-host", url.Values{"confirm_host_id": {"esc-host"}})
	if rr.Code != http.StatusConflict {
		t.Fatalf("escrow-unacked delete = %d, want 409", rr.Code)
	}
	if !strings.Contains(rr.Body.String(), "escrow") {
		t.Error("409 body must name the escrow so the operator knows what to acknowledge")
	}
	if h, _ := st.GetHost("esc-host"); h == nil {
		t.Fatal("host deleted despite missing escrow ack")
	}
	if e, _ := st.GetHostEscrow("esc-host"); e == nil {
		t.Fatal("escrow deleted despite missing ack")
	}
}

// D3 — type-to-confirm mismatch → 400, zero deletions.
func TestHostDelete_ConfirmMismatch(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.UpsertHost(&store.Host{HostID: "typo-host", CustomerID: "c3", APIKey: "k"}); err != nil {
		t.Fatal(err)
	}
	rr := postHostDelete(t, s, "typo-host", url.Values{"confirm_host_id": {"typo-hots"}})
	if rr.Code != http.StatusBadRequest {
		t.Fatalf("confirm mismatch = %d, want 400", rr.Code)
	}
	if h, _ := st.GetHost("typo-host"); h == nil {
		t.Fatal("host deleted despite confirm mismatch")
	}

	// Unknown host → 404.
	rr = postHostDelete(t, s, "ghost", url.Values{"confirm_host_id": {"ghost"}})
	if rr.Code != http.StatusNotFound {
		t.Errorf("unknown host delete = %d, want 404", rr.Code)
	}
}

// D4 — the impact probe returns the documented JSON shape: counts + booleans ONLY.
func TestHostDelete_ImpactJSON(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.UpsertHost(&store.Host{HostID: "imp-host", CustomerID: "c4", APIKey: "SECRET-KEY"}); err != nil {
		t.Fatal(err)
	}
	if _, _, err := st.SaveHostEscrow("imp-host", []byte("blob"), "fp", "p", "2026-07-01T00:00:00Z", ""); err != nil {
		t.Fatal(err)
	}
	if err := st.UpsertGuestFromReport(&store.Guest{GuestID: store.GuestID("imp-host", 100),
		CustomerID: "c4", HostID: "imp-host", VMID: 100, Status: "stopped"}); err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()
	s.handleHostDeleteImpact(rr, httptest.NewRequest(http.MethodGet, "/hosts/imp-host/delete-impact", nil), "imp-host")
	if rr.Code != http.StatusOK {
		t.Fatalf("impact = %d", rr.Code)
	}
	var d struct {
		Status           string `json:"status"`
		Deletable        bool   `json:"deletable"`
		Guests           int    `json:"guests"`
		Reports          int    `json:"reports"`
		LogBundles       int    `json:"log_bundles"`
		EscrowPresent    bool   `json:"escrow_present"`
		WGPeerBound      bool   `json:"wg_peer_bound"`
		PBSSecretPresent bool   `json:"pbs_secret_present"`
		RecoveryPresent  bool   `json:"recovery_present"`
	}
	if err := json.Unmarshal(rr.Body.Bytes(), &d); err != nil {
		t.Fatalf("impact JSON: %v", err)
	}
	if d.Status != "pending" || !d.Deletable || d.Guests != 1 || !d.EscrowPresent {
		t.Errorf("impact = %+v, want pending/deletable/guests=1/escrow", d)
	}
	// Booleans/counts only — never the api_key or blob bytes.
	if strings.Contains(rr.Body.String(), "SECRET-KEY") || strings.Contains(rr.Body.String(), "blob") {
		t.Error("SECRET LEAK: impact JSON carries a secret value")
	}

	// Unknown host → 404.
	rr = httptest.NewRecorder()
	s.handleHostDeleteImpact(rr, httptest.NewRequest(http.MethodGet, "/hosts/nope/delete-impact", nil), "nope")
	if rr.Code != http.StatusNotFound {
		t.Errorf("unknown impact = %d, want 404", rr.Code)
	}
}

// Scenario C (handler level) — a deletable host with escrow + ack: 303 to /hosts, rows gone.
func TestHostDelete_HappyPath(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.UpsertHost(&store.Host{HostID: "dr-drill", CustomerID: "c5", APIKey: "k"}); err != nil {
		t.Fatal(err)
	}
	if _, _, err := st.SaveHostEscrow("dr-drill", []byte("blob"), "fp", "p", "2026-07-01T00:00:00Z", ""); err != nil {
		t.Fatal(err)
	}
	rr := postHostDelete(t, s, "dr-drill", url.Values{
		"confirm_host_id": {"dr-drill"}, "delete_escrow": {"1"},
	})
	if rr.Code != http.StatusSeeOther {
		t.Fatalf("delete = %d (%s), want 303", rr.Code, rr.Body.String())
	}
	if loc := rr.Header().Get("Location"); loc != "/hosts" {
		t.Errorf("redirect = %q, want /hosts", loc)
	}
	if h, _ := st.GetHost("dr-drill"); h != nil {
		t.Fatal("host row survived the delete")
	}
	if e, _ := st.GetHostEscrow("dr-drill"); e != nil {
		t.Fatal("escrow row survived the acknowledged delete")
	}
}

// The danger-zone card renders ONLY for a non-online host. The ONLINE case is pinned by
// TestHandleHostDetail's exactly-2-buttons assertion (which now doubles as the
// "delete hidden for online hosts" proof).
func TestHostDetail_DangerCardForStaleOnly(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.UpsertHost(&store.Host{HostID: "junk-host", CustomerID: "c6", APIKey: "k"}); err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()
	s.handleHostDetail(rr, httptest.NewRequest(http.MethodGet, "/hosts/junk-host", nil), "junk-host")
	body := rr.Body.String()
	if !strings.Contains(body, "Danger zone") {
		t.Error("non-online host missing the danger-zone card")
	}
	if !strings.Contains(body, `action="/hosts/junk-host/delete"`) {
		t.Error("danger-zone card missing the delete form")
	}
	if !strings.Contains(body, "Re-enrollment requires the Day-0 passphrase flow") {
		t.Error("danger copy must state the consequence")
	}
}
