package web

// Group C (S2) + Group D (v0.47.0 multi-endpoint management) — the /offsite page.
// v0.47.0 amends the S2 render pins DELIBERATELY for the multi-card layout + Endpoint
// column; the pubkey-title and empty-/hosts/-href assertions are kept verbatim.

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

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

func renderOffsite(t *testing.T, s *Server) string {
	t.Helper()
	rr := httptest.NewRecorder()
	s.handleOffsite(rr, httptest.NewRequest(http.MethodGet, "/offsite", nil))
	if rr.Code != http.StatusOK {
		t.Fatalf("offsite render = %d", rr.Code)
	}
	return rr.Body.String()
}

func postOffsiteEndpoint(t *testing.T, s *Server, form url.Values) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, "/offsite/endpoints", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	rr := httptest.NewRecorder()
	s.handleOffsiteEndpointSave(rr, req)
	return rr
}

// validEndpointForm returns a fully-valid add form; tests mutate single fields.
func validEndpointForm(id string) url.Values {
	return url.Values{
		"endpoint_id":   {id},
		"dns_name":      {id + ".felhom.eu"},
		"wg_port":       {"443"},
		"server_pubkey": {"CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk="},
		"tunnel_subnet": {"10.78.0.0/24"},
		"pbs_tunnel_ip": {"10.78.0.1"},
	}
}

func TestOffsite_NoEndpoint(t *testing.T) {
	s, _ := newTestServer(t)
	html := renderOffsite(t, s)
	if !strings.Contains(html, "Not configured") {
		t.Errorf("no-endpoint state missing: %s", html[:200])
	}
	if !strings.Contains(html, "No WireGuard peers registered") {
		t.Errorf("empty-peers state missing")
	}
	// The add-endpoint form is offered even before the first endpoint exists.
	if !strings.Contains(html, `action="/offsite/endpoints"`) {
		t.Errorf("add-endpoint form missing")
	}
}

// E1 — two endpoint cards render (ids visible), per-endpoint peer counts derive from
// subnet membership, and the peer table's Endpoint column shows the containing endpoint.
func TestOffsite_EndpointAndPeers(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.SetWGEndpoint(&store.WGEndpoint{
		EndpointID: "ep0", DNSName: "ep0.felhom.eu", WGPort: 443,
		ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=",
		TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1",
	}); err != nil {
		t.Fatal(err)
	}
	if err := st.SetWGEndpoint(&store.WGEndpoint{
		EndpointID: "ep1", DNSName: "ep1.felhom.eu", WGPort: 51820,
		ServerPubkey: "CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo=",
		TunnelSubnet: "10.78.0.0/24", PBSTunnelIP: "10.78.0.1",
	}); err != nil {
		t.Fatal(err)
	}
	st.UpsertHost(&store.Host{HostID: "hv1", CustomerID: "c1", APIKey: "k"})
	if _, _, err := st.RegisterWGPeerForHost("hv1", "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="); err != nil {
		t.Fatal(err)
	}
	if _, _, err := st.AddWGPeer("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", "", "unbound-test"); err != nil {
		t.Fatal(err)
	}

	html := renderOffsite(t, s)
	for _, want := range []string{
		`data-endpoint-id="ep0"`, // ep0 card
		`data-endpoint-id="ep1"`, // ep1 card
		"ep0.felhom.eu:443",      // ep0 address
		"ep1.felhom.eu:51820",    // ep1 address
		"10.77.0.0/24",           // ep0 subnet
		"10.77.0.1:8007",         // ep0 PBS tunnel addr
		"10.77.0.2/32",           // bound peer ip
		"10.77.0.3/32",           // unbound peer ip
		`href="/hosts/hv1"`,      // bound peer links to its host
		"unbound-test",           // note column
	} {
		if !strings.Contains(html, want) {
			t.Errorf("offsite page missing %q", want)
		}
	}
	// Per-endpoint peer counts: both allocated peers live in ep0's subnet; ep1 has none.
	ep0Card := html[strings.Index(html, `data-endpoint-id="ep0"`):strings.Index(html, `data-endpoint-id="ep1"`)]
	ep1Card := html[strings.Index(html, `data-endpoint-id="ep1"`):]
	if !strings.Contains(ep0Card, "<td>2</td>") {
		t.Error("ep0 card missing peer count 2")
	}
	if !strings.Contains(ep1Card, "<td>0</td>") {
		t.Error("ep1 card missing peer count 0")
	}
	// The peer table's Endpoint column shows ep0 for both peers.
	if got := strings.Count(html, "<td><code>ep0</code></td>"); got != 2 {
		t.Errorf("Endpoint column shows ep0 %d times, want 2", got)
	}
	// Unbound peer renders an em-dash host cell, not a broken link.
	if strings.Contains(html, `href="/hosts/"`) {
		t.Error("unbound peer rendered an empty host link")
	}
	// Full pubkey rides the title attribute (truncated display).
	if !strings.Contains(html, `title="AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="`) {
		t.Error("full pubkey missing from title attr")
	}
}

// E5 — a peer whose IP falls in NO endpoint's subnet renders Endpoint "—".
func TestOffsite_OrphanPeerEndpointColumn(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.SetWGEndpoint(&store.WGEndpoint{
		EndpointID: "ep0", DNSName: "ep0.felhom.eu", WGPort: 443,
		ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=",
		TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1",
	}); err != nil {
		t.Fatal(err)
	}
	if _, _, err := st.AddWGPeer("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", "", "orphan"); err != nil {
		t.Fatal(err) // allocates 10.77.0.2
	}
	// Replace ep0 with an endpoint on a DIFFERENT subnet → the peer's IP matches nothing.
	if err := st.DeleteWGEndpoint("ep0"); err != nil {
		t.Fatal(err)
	}
	if err := st.SetWGEndpoint(&store.WGEndpoint{
		EndpointID: "ep1", DNSName: "ep1.felhom.eu", WGPort: 443,
		ServerPubkey: "CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo=",
		TunnelSubnet: "10.78.0.0/24", PBSTunnelIP: "10.78.0.1",
	}); err != nil {
		t.Fatal(err)
	}

	html := renderOffsite(t, s)
	if !strings.Contains(html, "10.77.0.2/32") {
		t.Fatal("orphan peer missing from the table")
	}
	if strings.Contains(html, "<td><code>ep1</code></td>") {
		t.Error("orphan peer wrongly attributed to ep1 — must render em dash")
	}
}

// E2 — a valid POST upserts via SetWGEndpoint and redirects back to the page.
func TestOffsiteEndpointSave_Add(t *testing.T) {
	s, st := newTestServer(t)
	rr := postOffsiteEndpoint(t, s, validEndpointForm("ep1"))
	if rr.Code != http.StatusSeeOther {
		t.Fatalf("add = %d (%s), want 303", rr.Code, rr.Body.String())
	}
	eps, err := st.ListWGEndpoints()
	if err != nil || len(eps) != 1 {
		t.Fatalf("endpoints after add = %v / %v, want 1", eps, err)
	}
	if eps[0].EndpointID != "ep1" || eps[0].DNSName != "ep1.felhom.eu" || eps[0].WGPort != 443 ||
		eps[0].TunnelSubnet != "10.78.0.0/24" || eps[0].PBSTunnelIP != "10.78.0.1" {
		t.Errorf("stored endpoint = %+v", eps[0])
	}
	// The new card renders.
	if !strings.Contains(renderOffsite(t, s), `data-endpoint-id="ep1"`) {
		t.Error("new endpoint card missing from the page")
	}
}

// E6 — every invalid form field → 400 and NOTHING stored.
func TestOffsiteEndpointSave_Validation(t *testing.T) {
	s, st := newTestServer(t)
	cases := []struct {
		name  string
		field string
		value string
	}{
		{"bad cidr", "tunnel_subnet", "10.78.0.0/240"},
		{"not a cidr", "tunnel_subnet", "banana"},
		{"pbs ip outside subnet", "pbs_tunnel_ip", "10.99.0.1"},
		{"pbs ip garbage", "pbs_tunnel_ip", "not-an-ip"},
		{"port zero", "wg_port", "0"},
		{"port too big", "wg_port", "70000"},
		{"port garbage", "wg_port", "abc"},
		{"empty pubkey", "server_pubkey", ""},
		{"bad endpoint id", "endpoint_id", "EP 1!"},
		{"empty endpoint id", "endpoint_id", ""},
		{"empty dns", "dns_name", ""},
	}
	for _, c := range cases {
		form := validEndpointForm("ep1")
		form.Set(c.field, c.value)
		rr := postOffsiteEndpoint(t, s, form)
		if rr.Code != http.StatusBadRequest {
			t.Errorf("%s: status = %d, want 400", c.name, rr.Code)
		}
		if eps, _ := st.ListWGEndpoints(); len(eps) != 0 {
			t.Fatalf("%s: endpoint STORED despite invalid input: %+v", c.name, eps)
		}
	}
}

// E3 — editing ep0's tunnel_subnet while peers are allocated inside the CURRENT subnet is
// refused with 409 and the stored subnet is UNCHANGED.
// RED-PROOF 4: removing the subnet-change guard makes this FAIL (subnet rewritten).
func TestOffsiteEndpointSave_SubnetChangeGuard(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.SetWGEndpoint(&store.WGEndpoint{
		EndpointID: "ep0", DNSName: "ep0.felhom.eu", WGPort: 443,
		ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=",
		TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1",
	}); err != nil {
		t.Fatal(err)
	}
	if _, _, err := st.AddWGPeer("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", "", "pin"); err != nil {
		t.Fatal(err) // 10.77.0.2 — inside the current subnet
	}

	form := validEndpointForm("ep0")
	form.Set("tunnel_subnet", "10.99.0.0/24")
	form.Set("pbs_tunnel_ip", "10.99.0.1")
	rr := postOffsiteEndpoint(t, s, form)
	if rr.Code != http.StatusConflict {
		t.Fatalf("subnet change with peers = %d, want 409", rr.Code)
	}
	ep, err := st.GetWGEndpoint()
	if err != nil {
		t.Fatal(err)
	}
	if ep.TunnelSubnet != "10.77.0.0/24" {
		t.Errorf("stored subnet = %s — the refused edit CHANGED it", ep.TunnelSubnet)
	}

	// Non-subnet edits stay allowed while peers exist (e.g. a port move).
	form = validEndpointForm("ep0")
	form.Set("tunnel_subnet", "10.77.0.0/24")
	form.Set("pbs_tunnel_ip", "10.77.0.1")
	form.Set("wg_port", "51820")
	if rr := postOffsiteEndpoint(t, s, form); rr.Code != http.StatusSeeOther {
		t.Errorf("same-subnet edit = %d, want 303", rr.Code)
	}
	if ep, _ := st.GetWGEndpoint(); ep.WGPort != 51820 {
		t.Errorf("port edit not stored: %+v", ep)
	}
}

// E4 — deleting an endpoint with peers allocated in its subnet is refused with 409 and the
// row survives; a peer-free endpoint deletes cleanly; an unknown one 404s.
// RED-PROOF 3: removing the peers-in-subnet guard makes this FAIL (endpoint deleted).
func TestOffsiteEndpointDelete_Guard(t *testing.T) {
	s, st := newTestServer(t)
	if err := st.SetWGEndpoint(&store.WGEndpoint{
		EndpointID: "ep0", DNSName: "ep0.felhom.eu", WGPort: 443,
		ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=",
		TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1",
	}); err != nil {
		t.Fatal(err)
	}
	if err := st.SetWGEndpoint(&store.WGEndpoint{
		EndpointID: "ep1", DNSName: "ep1.felhom.eu", WGPort: 443,
		ServerPubkey: "CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo=",
		TunnelSubnet: "10.78.0.0/24", PBSTunnelIP: "10.78.0.1",
	}); err != nil {
		t.Fatal(err)
	}
	if _, _, err := st.AddWGPeer("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", "", "pin"); err != nil {
		t.Fatal(err) // 10.77.0.2 — pins ep0
	}

	del := func(id string) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPost, "/offsite/endpoints/"+id+"/delete", nil)
		rr := httptest.NewRecorder()
		s.handleOffsiteEndpointDelete(rr, req, id)
		return rr
	}

	if rr := del("ep0"); rr.Code != http.StatusConflict {
		t.Fatalf("delete with peers = %d, want 409", rr.Code)
	}
	if eps, _ := st.ListWGEndpoints(); len(eps) != 2 {
		t.Fatalf("endpoint count after refused delete = %d, want 2 (ep0 must survive)", len(eps))
	}

	if rr := del("ep1"); rr.Code != http.StatusSeeOther {
		t.Errorf("peer-free delete = %d, want 303", rr.Code)
	}
	if eps, _ := st.ListWGEndpoints(); len(eps) != 1 || eps[0].EndpointID != "ep0" {
		t.Errorf("endpoints after ep1 delete = %+v, want [ep0]", eps)
	}

	if rr := del("ghost"); rr.Code != http.StatusNotFound {
		t.Errorf("unknown endpoint delete = %d, want 404", rr.Code)
	}
}
