package main

import (
	"net/netip"
	"strings"
	"testing"
)

// loadSeed prefers inventory.yaml; falls back to radios.yaml during the
// migration window. Tests use whichever the repo currently ships.
func loadSeed(t *testing.T) *seed {
	t.Helper()
	s, err := loadSeedInventory("inventory.yaml")
	if err != nil {
		t.Fatalf("loadSeedInventory: %v", err)
	}
	return s
}

func TestLoadSeedInventory(t *testing.T) {
	s := loadSeed(t)
	wantSites := []string{"verona", "altoga", "climax", "culleoka", "newhope", "lowry", "982", "494", "core"}
	if len(s.Order) != len(wantSites) {
		t.Fatalf("site order = %v, want %v", s.Order, wantSites)
	}
	for i, name := range wantSites {
		if s.Order[i] != name {
			t.Fatalf("site[%d] = %q, want %q", i, s.Order[i], name)
		}
	}

	verona := s.Sites["verona"]
	if verona.Router != "10.254.254.101" {
		t.Errorf("verona router = %q", verona.Router)
	}
	if verona.MgmtSubnet.String() != "10.10.0.0/20" {
		t.Errorf("verona mgmt = %q", verona.MgmtSubnet)
	}

	altoga := s.Sites["altoga"]
	if altoga.ParentRouter != "10.254.254.101" {
		t.Errorf("altoga parent_router = %q", altoga.ParentRouter)
	}
	if altoga.Router != "" {
		t.Errorf("altoga router = %q, want empty", altoga.Router)
	}

	// Backhaul link labels indexed by /29.
	climax := s.Sites["climax"]
	pfx := netip.MustParsePrefix("10.250.1.24/29").Masked()
	if got := climax.LinkLabels[pfx]; got != "verona<->climax AF11" {
		t.Errorf("climax link for %s = %q", pfx, got)
	}
}

func TestTopSlash24(t *testing.T) {
	cases := map[string]string{
		"10.10.0.0/20":   "10.10.15.0/24",
		"10.10.16.0/20":  "10.10.31.0/24",
		"10.10.80.0/20":  "10.10.95.0/24",
		"10.10.96.0/20":  "10.10.111.0/24",
		"10.10.128.0/20": "10.10.143.0/24",
		"10.10.144.0/20": "10.10.159.0/24",
		"10.10.160.0/20": "10.10.175.0/24",
	}
	for in, want := range cases {
		got := topSlash24(netip.MustParsePrefix(in))
		if got.String() != want {
			t.Errorf("topSlash24(%s) = %s, want %s", in, got, want)
		}
	}
}

func TestClosestRouterIP(t *testing.T) {
	owners := map[netip.Addr]*routerDiscovery{
		netip.MustParseAddr("10.250.1.25"): nil,
		netip.MustParseAddr("10.250.1.30"): nil,
	}
	got := closestRouterIP(netip.MustParseAddr("10.250.1.26"), owners)
	if got.String() != "10.250.1.25" {
		t.Errorf("closest to .26 = %s, want .25", got)
	}
	got = closestRouterIP(netip.MustParseAddr("10.250.1.29"), owners)
	if got.String() != "10.250.1.30" {
		t.Errorf("closest to .29 = %s, want .30", got)
	}
}

func TestHasCap(t *testing.T) {
	cases := []struct {
		caps, want string
		expect     bool
	}{
		{"bridge", "bridge", true},
		{"bridge,router", "bridge", true},
		{"bridge,wlan-ap,router,station-only", "wlan-ap", true},
		{"bridge,router", "wlan-ap", false},
		{"", "bridge", false},
	}
	for _, c := range cases {
		if got := hasCap(c.caps, c.want); got != c.expect {
			t.Errorf("hasCap(%q, %q) = %v, want %v", c.caps, c.want, got, c.expect)
		}
	}
}

func TestCustomerPoolName(t *testing.T) {
	want := []string{"verona-cpe", "altoga-cgnat", "cgnat", "cgnat-full", "DHCP_pool3", "customer-stuff"}
	dont := []string{"verona-tower-pool", "verona-v6-pd-pool", "altoga-old"}
	for _, n := range want {
		if !customerPoolName(n) {
			t.Errorf("customerPoolName(%q) = false, want true", n)
		}
	}
	for _, n := range dont {
		if customerPoolName(n) {
			t.Errorf("customerPoolName(%q) = true, want false", n)
		}
	}
}

func TestPoolRangeContains(t *testing.T) {
	p := poolRange{
		Lo: netip.MustParseAddr("10.10.0.1"),
		Hi: netip.MustParseAddr("10.10.14.254"),
	}
	if !p.contains(netip.MustParseAddr("10.10.5.5")) {
		t.Errorf("expected 10.10.5.5 in range")
	}
	if p.contains(netip.MustParseAddr("10.10.15.1")) {
		t.Errorf("10.10.15.1 should be outside")
	}
}

func TestCollectSwitches(t *testing.T) {
	d := &routerDiscovery{
		Router: Router{Name: "culleoka", Host: "10.254.254.104"},
		OwnIP:  netip.MustParseAddr("10.254.254.104"),
		Addresses: []addrEntry{
			// directly-connected subnets we accept switches on
			{Prefix: netip.MustParsePrefix("10.10.96.0/20")},
			{Prefix: netip.MustParsePrefix("100.64.24.0/22")},
		},
		Neighbors: []neighborEntry{
			// Real Netonix switch on a directly-connected /22 — include.
			{Addr: netip.MustParseAddr("100.64.27.250"), MAC: "EC:13:B2:A4:05:CA",
				Identity: "Culleoka Switch", Platform: "Netonix WS-26-400-AC", Caps: "bridge"},
			// AP radio — wlan-ap capability, exclude.
			{Addr: netip.MustParseAddr("10.10.111.11"), MAC: "78:45:58:A2:EC:F9",
				Identity: "Culleoka SE", Platform: "EPMP3000", Caps: "bridge,wlan-ap,router,station-only"},
			// Customer-side LAN — outside our address space, exclude.
			{Addr: netip.MustParseAddr("192.168.1.5"), MAC: "18:FD:74:8F:0C:06",
				Identity: "Customer Router", Platform: "MikroTik", Caps: "bridge,router"},
			// Leaked via tunnel — IP not in any directly-connected prefix, exclude.
			{Addr: netip.MustParseAddr("10.250.2.2"), MAC: "B8:69:F4:12:8E:F8",
				Identity: "core to office", Platform: "MikroTik", Caps: "bridge,router"},
			// One of our other routers — exclude via routerIPs map.
			{Addr: netip.MustParseAddr("10.254.254.253"), MAC: "64:D1:54:EF:AD:7D",
				Identity: "Core", Platform: "MikroTik", Caps: "bridge,router"},
		},
	}
	routerIPs := map[netip.Addr]bool{
		netip.MustParseAddr("10.254.254.253"): true,
		netip.MustParseAddr("10.254.254.104"): true,
	}
	switches := collectSwitches(d, routerIPs)
	if len(switches) != 1 {
		t.Fatalf("expected 1 switch, got %d: %+v", len(switches), switches)
	}
	if switches[0].Name != "Culleoka Switch" || switches[0].Model != "Netonix WS-26-400-AC" {
		t.Errorf("unexpected switch: %+v", switches[0])
	}
}

func TestRenderYAMLSampleShape(t *testing.T) {
	s := loadSeed(t)
	site := &outSite{
		Name:       "culleoka",
		Router:     "10.254.254.104",
		MgmtSubnet: netip.MustParsePrefix("10.10.96.0/20"),
		Cgnats: []cgnat{
			{Subnet: netip.MustParsePrefix("100.64.24.0/22"), Gateway: netip.MustParseAddr("100.64.27.254")},
		},
		AccessPoints: []outRadio{
			{IP: netip.MustParseAddr("10.10.111.1"), MAC: "78:45:58:A0:03:F3", Name: "Culleoka SW AC-1"},
		},
		BackhaulRadios: []outRadio{
			{IP: netip.MustParseAddr("10.250.1.10"), MAC: "7A:8A:20:5F:49:7A",
				Name: "Culleoka to Climax 11ghz", Link: "climax<->culleoka AF11 (disabled)"},
		},
		Switches: []outSwitch{
			{IP: netip.MustParseAddr("100.64.27.250"), MAC: "EC:13:B2:A4:05:CA",
				Name: "Culleoka Switch", Model: "Netonix WS-26-400-AC"},
		},
	}
	body, err := renderYAML(s, []*outSite{site})
	if err != nil {
		t.Fatal(err)
	}
	got := string(body)
	for _, want := range []string{
		"sites:",
		"culleoka:",
		"router: 10.254.254.104",
		"mgmt_subnet: 10.10.96.0/20",
		"cgnat_subnet: {subnet: 100.64.24.0/22, gateway: 100.64.27.254}",
		"access_points:",
		`{ip: 10.10.111.1, mac: "78:45:58:A0:03:F3", name: "Culleoka SW AC-1"}`,
		"backhaul_radios:",
		`link: "climax<->culleoka AF11 (disabled)"`,
		"switches:",
		`{ip: 100.64.27.250, mac: "EC:13:B2:A4:05:CA", name: "Culleoka Switch", model: "Netonix WS-26-400-AC"}`,
	} {
		if !strings.Contains(got, want) {
			t.Errorf("rendered yaml missing %q\n--- output ---\n%s", want, got)
		}
	}
}
