package store

// Group A — the /32 allocator (S1, doc 06 §3.2). Non-hollow: every test asserts allocation
// EFFECTS (exact IPs, row counts, typed errors), not just nil-errors.

import (
	"database/sql"
	"testing"
)

func setTestEndpoint(t *testing.T, s *Store, subnet, pbsIP string) {
	t.Helper()
	err := s.SetWGEndpoint(&WGEndpoint{
		DNSName: "ep0.example", WGPort: 443, ServerPubkey: "SPK",
		TunnelSubnet: subnet, PBSTunnelIP: pbsIP,
	})
	if err != nil {
		t.Fatalf("SetWGEndpoint: %v", err)
	}
}

func peerCount(t *testing.T, s *Store) int {
	t.Helper()
	peers, err := s.ListWGPeers()
	if err != nil {
		t.Fatalf("ListWGPeers: %v", err)
	}
	return len(peers)
}

func TestWGEndpoint_UpsertAndGet(t *testing.T) {
	s := newTestStore(t)
	if _, err := s.GetWGEndpoint(); err != sql.ErrNoRows {
		t.Fatalf("unset endpoint: err = %v, want sql.ErrNoRows", err)
	}
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	e, err := s.GetWGEndpoint()
	if err != nil || e.EndpointID != "ep0" || e.TunnelSubnet != "10.77.0.0/24" || e.WGPort != 443 {
		t.Fatalf("GetWGEndpoint = %+v, %v", e, err)
	}
	// Upsert overwrites, same row.
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	e2, err := s.GetWGEndpoint()
	if err != nil || e2.EndpointID != "ep0" {
		t.Fatalf("after upsert: %+v, %v", e2, err)
	}
}

func TestAddWGPeer_SequentialAllocationSkipsNetPBSAndBroadcast(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")

	// .0 network and .1 endpoint are skipped → first three peers get .2 .3 .4 exactly.
	want := []string{"10.77.0.2", "10.77.0.3", "10.77.0.4"}
	for i, pk := range []string{"p1", "p2", "p3"} {
		ip, existed, err := s.AddWGPeer(pk, "", "")
		if err != nil || existed {
			t.Fatalf("add %s: ip=%q existed=%v err=%v", pk, ip, existed, err)
		}
		if ip != want[i] {
			t.Errorf("peer %s ip = %q, want %q", pk, ip, want[i])
		}
	}
}

func TestAddWGPeer_FreedIPReused(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	for _, pk := range []string{"p1", "p2", "p3"} {
		s.AddWGPeer(pk, "", "")
	}
	if err := s.RemoveWGPeer("p2"); err != nil {
		t.Fatalf("RemoveWGPeer(p2): %v", err)
	}
	ip, existed, err := s.AddWGPeer("p4", "", "")
	if err != nil || existed {
		t.Fatalf("add p4: %v existed=%v", err, existed)
	}
	if ip != "10.77.0.3" {
		t.Errorf("p4 ip = %q, want the freed lowest 10.77.0.3", ip)
	}
}

func TestAddWGPeer_IdempotentOnExistingPubkey(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	ip1, _, err := s.AddWGPeer("p1", "", "")
	if err != nil {
		t.Fatalf("first add: %v", err)
	}
	before := peerCount(t, s)
	ip2, existed, err := s.AddWGPeer("p1", "", "")
	if err != nil || !existed {
		t.Fatalf("re-add: err=%v existed=%v, want idempotent hit", err, existed)
	}
	if ip2 != ip1 {
		t.Errorf("re-add ip = %q, want unchanged %q", ip2, ip1)
	}
	if after := peerCount(t, s); after != before {
		t.Errorf("row count changed on idempotent re-add: %d -> %d", before, after)
	}
}

func TestAddWGPeer_NoEndpoint(t *testing.T) {
	s := newTestStore(t)
	_, _, err := s.AddWGPeer("p1", "", "")
	if err != ErrWGEndpointUnset {
		t.Fatalf("err = %v, want ErrWGEndpointUnset", err)
	}
	if n := peerCount(t, s); n != 0 {
		t.Errorf("peer rows created without endpoint: %d", n)
	}
}

func TestAddWGPeer_SubnetExhausted(t *testing.T) {
	s := newTestStore(t)
	// /30: .0 network, .1 endpoint, .2 the only host, .3 broadcast.
	setTestEndpoint(t, s, "10.77.0.0/30", "10.77.0.1")
	ip, _, err := s.AddWGPeer("p1", "", "")
	if err != nil || ip != "10.77.0.2" {
		t.Fatalf("p1: ip=%q err=%v, want the single free host .2", ip, err)
	}
	_, _, err = s.AddWGPeer("p2", "", "")
	if err != ErrWGSubnetExhausted {
		t.Fatalf("p2 err = %v, want ErrWGSubnetExhausted", err)
	}
	if n := peerCount(t, s); n != 1 {
		t.Errorf("partial row on exhaustion: count = %d, want 1", n)
	}
}

func TestRemoveWGPeer_UnknownIsNoRows(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	if err := s.RemoveWGPeer("ghost"); err != sql.ErrNoRows {
		t.Fatalf("err = %v, want sql.ErrNoRows", err)
	}
}

// --- S2 Group A: host binding + generation bump ---

func TestRegisterWGPeerForHost_FreshIdempotentAndRekey(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	if err := s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"}); err != nil {
		t.Fatal(err)
	}

	// Fresh → allocates .2, changed=true.
	ip, changed, err := s.RegisterWGPeerForHost("h1", "P1")
	if err != nil || ip != "10.77.0.2" || !changed {
		t.Fatalf("fresh register = %q changed=%v err=%v, want .2/true", ip, changed, err)
	}
	// Idempotent: same pubkey → same ip, changed=false.
	ip, changed, err = s.RegisterWGPeerForHost("h1", "P1")
	if err != nil || ip != "10.77.0.2" || changed {
		t.Fatalf("idempotent register = %q changed=%v err=%v, want .2/false", ip, changed, err)
	}
	// Re-key: NEW pubkey → swapped in place, ip KEPT, changed=true.
	ip, changed, err = s.RegisterWGPeerForHost("h1", "P2")
	if err != nil || ip != "10.77.0.2" || !changed {
		t.Fatalf("re-key = %q changed=%v err=%v, want .2 kept/true", ip, changed, err)
	}
	p, err := s.GetWGPeerForHost("h1")
	if err != nil || p.Pubkey != "P2" || p.AssignedIP != "10.77.0.2" {
		t.Fatalf("after re-key peer = %+v err=%v", p, err)
	}
	if n := peerCount(t, s); n != 1 {
		t.Errorf("row count after re-key = %d, want 1", n)
	}
}

func TestRegisterWGPeerForHost_AdoptsUnboundRow(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"})
	// An S1 admin-added UNBOUND peer.
	ip0, _, err := s.AddWGPeer("P1", "", "s1-test")
	if err != nil {
		t.Fatal(err)
	}
	ip, changed, err := s.RegisterWGPeerForHost("h1", "P1")
	if err != nil || !changed || ip != ip0 {
		t.Fatalf("adopt = %q changed=%v err=%v, want %q/true", ip, changed, err, ip0)
	}
	p, _ := s.GetWGPeerForHost("h1")
	if p == nil || p.Pubkey != "P1" {
		t.Fatalf("adopted peer = %+v", p)
	}
}

func TestRegisterWGPeerForHost_BoundElsewhereRefused(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"})
	s.UpsertHost(&Host{HostID: "h2", CustomerID: "c2", APIKey: "k2"})
	s.RegisterWGPeerForHost("h2", "P1")

	// Fresh-registration path: pubkey bound to h2 → refused.
	if _, _, err := s.RegisterWGPeerForHost("h1", "P1"); err != ErrWGPubkeyBoundElsewhere {
		t.Fatalf("register with h2's pubkey: err = %v, want ErrWGPubkeyBoundElsewhere", err)
	}
	if p, err := s.GetWGPeerForHost("h1"); err != sql.ErrNoRows {
		t.Errorf("h1 gained a peer despite refusal: %+v (err=%v)", p, err)
	}
	// Re-key path: h1 registers its own, then tries to re-key to h2's pubkey → refused, binding intact.
	s.RegisterWGPeerForHost("h1", "P9")
	if _, _, err := s.RegisterWGPeerForHost("h1", "P1"); err != ErrWGPubkeyBoundElsewhere {
		t.Fatalf("re-key onto h2's pubkey: err = %v, want ErrWGPubkeyBoundElsewhere", err)
	}
	p, _ := s.GetWGPeerForHost("h1")
	if p == nil || p.Pubkey != "P9" {
		t.Errorf("h1 binding disturbed by refused re-key: %+v", p)
	}
}

func TestWGPeers_OneBoundPerHostIndex(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"})
	s.RegisterWGPeerForHost("h1", "P1")
	// A second BOUND row for the same host via the S1 admin path → the partial index refuses.
	if _, _, err := s.AddWGPeer("P2", "h1", ""); err == nil {
		t.Fatal("second bound peer for h1 accepted — idx_wg_peers_host is not enforcing")
	}
	// Unbound rows are unaffected by the partial index.
	if _, _, err := s.AddWGPeer("P3", "", ""); err != nil {
		t.Fatalf("unbound add refused: %v", err)
	}
}

func TestBumpHostDesired(t *testing.T) {
	s := newTestStore(t)
	s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"})
	gen, err := s.BumpHostDesired("h1")
	if err != nil || gen != 1 {
		t.Fatalf("bump #1 = %d, %v", gen, err)
	}
	gen, err = s.BumpHostDesired("h1")
	if err != nil || gen != 2 {
		t.Fatalf("bump #2 = %d, %v", gen, err)
	}
	if _, err := s.BumpHostDesired("ghost"); err != sql.ErrNoRows {
		t.Errorf("unknown host bump err = %v, want ErrNoRows", err)
	}
	// desired_json untouched by the bump.
	h, _ := s.GetHost("h1")
	if h.DesiredJSON != "{}" && h.DesiredJSON != "" {
		t.Errorf("desired_json moved on bump: %q", h.DesiredJSON)
	}
}

func TestListWGPeers_DeterministicOrder(t *testing.T) {
	s := newTestStore(t)
	setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
	s.AddWGPeer("pA", "h1", "note-a")
	s.AddWGPeer("pB", "", "")
	peers, err := s.ListWGPeers()
	if err != nil || len(peers) != 2 {
		t.Fatalf("ListWGPeers: %v (%d)", err, len(peers))
	}
	if peers[0].Pubkey != "pA" || peers[0].AssignedIP != "10.77.0.2" || peers[0].HostID != "h1" {
		t.Errorf("peers[0] = %+v", peers[0])
	}
	if peers[1].AssignedIP != "10.77.0.3" {
		t.Errorf("peers[1] = %+v", peers[1])
	}
}
