package web

// DR-tier-by-default (v0.51.0) — the three §10 contracts of the batch spec:
//   1. coupling gate: offsite provisioning REQUIRES the DR tier (drill F-6 closed by policy) —
//      exact refusal message; DR ON proceeds to the existing path.
//      Red-proof partner: remove the guard in applyOffsite → the refused-case test FAILS.
//   2. flag default: the NEW-customer form renders the DR checkbox CHECKED (default ON);
//      a legacy customer without an applied descriptor stays OFF (backfill covered store-side).
//      Red-proof partner: flip handleConfigNewForm's DRTier default → the form test FAILS.
//   3. capability chips: the host page renders the agent's capability list; "inactive" gets the
//      NEUTRAL badge (disabled ≠ degraded), degraded stays warn/error, and the pre-v1.15.0
//      "binary not found" pbsdr signature surfaces the migration hint.

import (
	"io"
	"log"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

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

const offsiteRequiresDRMsg = "Offsite backup requires the DR tier — enable it first (the escrow ceremony depends on the PBS key)"

// Scenario C — the coupling. DR OFF → offsite refused (exact message, Hetzner NEVER touched);
// DR ON → proceeds into the existing provisioning path.
func TestOffsiteRequiresDRTier(t *testing.T) {
	newOffsiteServer := func(t *testing.T) (*Server, *store.Store, *hetznerapi.Fake) {
		s, st := newTestServer(t)
		fakeAPI := hetznerapi.NewFake()
		s.SetOffsiteProvisioner(&offsite.Provisioner{
			API: fakeAPI, Store: st, Scanner: webTestScanner{},
			PoolBoxID: 611714, Location: "fsn1", Logger: log.New(io.Discard, "", 0),
		})
		if err := st.SaveCustomerConfig(&store.CustomerConfig{
			CustomerID: "cpl", CustomerName: "Coupling", Domain: "cpl.hu",
			RetrievalPassword: "pw", APIKey: "k", ConfigJSON: "{}",
		}); err != nil {
			t.Fatal(err)
		}
		return s, st, fakeAPI
	}
	post := func(s *Server, form url.Values) *httptest.ResponseRecorder {
		form.Set("customer_name", "Coupling")
		form.Set("domain", "cpl.hu")
		req := httptest.NewRequest("POST", "/configs/cpl", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		rr := httptest.NewRecorder()
		s.handleConfigUpdate(rr, req, "cpl")
		return rr
	}

	t.Run("DR OFF refuses with the exact message", func(t *testing.T) {
		s, st, _ := newOffsiteServer(t)
		rr := post(s, url.Values{"offsite_enabled": {"on"}, "offsite_type": {"shared"}, "offsite_quota_gb": {"50"}})
		if rr.Code != 502 {
			t.Fatalf("offsite with DR OFF = %d (%s), want 502 refusal", rr.Code, rr.Body.String())
		}
		if !strings.Contains(rr.Body.String(), offsiteRequiresDRMsg) {
			t.Fatalf("refusal message = %q, want the exact spec wording %q", rr.Body.String(), offsiteRequiresDRMsg)
		}
		// The F-6 dead end never starts: no descriptor, no one-time secret, nothing saved.
		cfg, _ := st.GetCustomerConfig("cpl")
		if strings.Contains(cfg.ConfigJSON, "offsite") {
			t.Errorf("offsite descriptor written despite the refusal: %s", cfg.ConfigJSON)
		}
		if _, err := st.ConsumeOneTimeSecret("cpl"); err == nil {
			t.Error("a one-time offsite secret exists despite the refusal")
		}
	})

	t.Run("DR ON proceeds", func(t *testing.T) {
		s, st, _ := newOffsiteServer(t)
		rr := post(s, url.Values{"dr_tier": {"on"}, "offsite_enabled": {"on"}, "offsite_type": {"shared"}, "offsite_quota_gb": {"50"}})
		if rr.Code != 303 {
			t.Fatalf("offsite with DR ON = %d (%s), want 303", rr.Code, rr.Body.String())
		}
		cfg, _ := st.GetCustomerConfig("cpl")
		if !strings.Contains(cfg.ConfigJSON, `"offsite"`) {
			t.Fatalf("offsite descriptor missing after a DR-ON provision: %s", cfg.ConfigJSON)
		}
	})
}

// Flag default: the NEW-customer form starts with the DR-tier checkbox CHECKED (decision 2 —
// DR is the default; opting out is the exception). The stored default for a customer saved
// WITHOUT the checkbox stays false (legacy shape — asserted store-side with the backfill).
func TestDRTier_NewCustomerFormDefaultsOn(t *testing.T) {
	s, _ := newTestServer(t)
	req := httptest.NewRequest("GET", "/configs/new", nil)
	rr := httptest.NewRecorder()
	s.handleConfigNewForm(rr, req)
	out := rr.Body.String()
	if !strings.Contains(out, `name="dr_tier" checked`) {
		t.Fatal("the new-customer form does not default the DR tier ON")
	}
}

// The host page's capability section (v0.51.0): inactive = neutral badge, degraded = warn/error,
// and the pre-v1.15.0 pbsdr "binary not found" signature surfaces the migration hint. Also the
// honest-reporting edge (§8): degraded is NEVER re-labeled by the hub.
func TestHostDetail_CapabilityChips(t *testing.T) {
	reportFor := func(status, reason string) string {
		return `{"host":{"cpu_percent":1},"capabilities":[
			{"name":"pbsdr-create","feature":"PBS DR storage-entry create","critical":false,"status":"` + status + `","reason":"` + reason + `"},
			{"name":"drive-bind","feature":"drive attach","critical":true,"status":"ok"}
		]}`
	}
	seed := func(t *testing.T, report string) (*Server, *store.Host) {
		s, st := newTestServer(t)
		if err := st.SaveCustomerConfig(&store.CustomerConfig{
			CustomerID: "capc", RetrievalPassword: "pw", APIKey: "k",
		}); err != nil {
			t.Fatal(err)
		}
		if err := st.UpsertHost(&store.Host{HostID: "capc-01", CustomerID: "capc", APIKey: "h"}); err != nil {
			t.Fatal(err)
		}
		if err := st.SaveHostReport("capc-01", "capc", []byte(report), store.HostReportDenorm{}); err != nil {
			t.Fatal(err)
		}
		host, err := st.GetHost("capc-01")
		if err != nil || host == nil {
			t.Fatal("seed host read")
		}
		return s, host
	}
	render := func(t *testing.T, s *Server, host *store.Host) string {
		data := s.hostDetailData(host, httptest.NewRequest("GET", "/hosts/capc-01", nil))
		var sb strings.Builder
		if err := s.templates.ExecuteTemplate(&sb, "host_detail_body", data); err != nil {
			t.Fatalf("render: %v", err)
		}
		return sb.String()
	}

	t.Run("inactive renders the neutral chip, not red", func(t *testing.T) {
		s, host := seed(t, reportFor("inactive", "disabled by configuration"))
		out := render(t, s, host)
		if !strings.Contains(out, `badge badge-neutral">inactive`) {
			t.Fatalf("inactive chip not neutral:\n%s", section(out, "Capabilities"))
		}
		if strings.Contains(out, `badge-error">inactive`) || strings.Contains(out, `badge-warn">inactive`) {
			t.Error("inactive rendered as an alarm state — disabled must not look broken")
		}
		if strings.Contains(out, "predates the uniform DR plumbing") {
			t.Error("migration hint shown for a healthy inactive box")
		}
	})

	t.Run("degraded binary-not-found stays degraded + migration hint", func(t *testing.T) {
		s, host := seed(t, reportFor("degraded", "binary not found"))
		out := render(t, s, host)
		if !strings.Contains(out, `badge badge-warn">degraded`) {
			t.Fatalf("pbsdr degraded chip missing/re-labeled:\n%s", section(out, "Capabilities"))
		}
		if !strings.Contains(out, "predates the uniform DR plumbing") {
			t.Error("migration hint missing for the pre-v1.15.0 signature (never silently pretend)")
		}
	})

	t.Run("ok renders ok; critical is marked", func(t *testing.T) {
		s, host := seed(t, reportFor("ok", ""))
		out := render(t, s, host)
		if !strings.Contains(out, `badge badge-ok">ok`) {
			t.Fatalf("ok chip missing:\n%s", section(out, "Capabilities"))
		}
		if !strings.Contains(out, `badge badge-neutral">critical`) {
			t.Error("critical marker missing on drive-bind")
		}
	})
}

// section extracts a readable slice of rendered HTML around a heading for failure messages.
func section(html, heading string) string {
	i := strings.Index(html, heading)
	if i < 0 {
		return "(heading not found)"
	}
	end := i + 1500
	if end > len(html) {
		end = len(html)
	}
	return html[i:end]
}
