package networking

import (
	"reflect"
	"strings"
	"testing"
)

func TestParseInterfaces(t *testing.T) {
	// Trimmed real output from `ip -j addr` on a typical host.
	input := `[
  {
    "ifname": "lo",
    "address": "00:00:00:00:00:00",
    "mtu": 65536,
    "operstate": "UNKNOWN",
    "addr_info": [
      {"family": "inet", "local": "127.0.0.1", "prefixlen": 8},
      {"family": "inet6", "local": "::1", "prefixlen": 128}
    ]
  },
  {
    "ifname": "eth0",
    "address": "52:54:00:12:34:56",
    "mtu": 1500,
    "operstate": "UP",
    "addr_info": [
      {"family": "inet", "local": "192.168.1.10", "prefixlen": 24},
      {"family": "inet6", "local": "fe80::1", "prefixlen": 64}
    ]
  }
]`

	ifaces, err := parseInterfaces(input)
	if err != nil {
		t.Fatal(err)
	}
	if len(ifaces) != 2 {
		t.Fatalf("expected 2 interfaces, got %d", len(ifaces))
	}

	lo := ifaces[0]
	if lo.Name != "lo" || lo.State != "unknown" || lo.MTU != 65536 {
		t.Errorf("lo: got %+v", lo)
	}
	if len(lo.IPv4) != 1 || lo.IPv4[0] != "127.0.0.1/8" {
		t.Errorf("lo ipv4: got %v", lo.IPv4)
	}

	eth0 := ifaces[1]
	if eth0.Name != "eth0" || eth0.State != "up" || eth0.MAC != "52:54:00:12:34:56" {
		t.Errorf("eth0: got %+v", eth0)
	}
	if len(eth0.IPv4) != 1 || eth0.IPv4[0] != "192.168.1.10/24" {
		t.Errorf("eth0 ipv4: got %v", eth0.IPv4)
	}
	if len(eth0.IPv6) != 1 || eth0.IPv6[0] != "fe80::1/64" {
		t.Errorf("eth0 ipv6: got %v", eth0.IPv6)
	}
}

func TestParseRoutes(t *testing.T) {
	input := `[
  {"dst": "default", "gateway": "192.168.1.1", "dev": "eth0", "prefsrc": "192.168.1.10", "metric": 100},
  {"dst": "192.168.1.0/24", "dev": "eth0", "prefsrc": "192.168.1.10"}
]`

	routes, err := parseRoutes(input)
	if err != nil {
		t.Fatal(err)
	}
	if len(routes) != 2 {
		t.Fatalf("expected 2 routes, got %d", len(routes))
	}
	if routes[0].Destination != "default" || routes[0].Gateway != "192.168.1.1" || routes[0].Metric != 100 {
		t.Errorf("route 0: got %+v", routes[0])
	}
	if routes[1].Destination != "192.168.1.0/24" || routes[1].Interface != "eth0" {
		t.Errorf("route 1: got %+v", routes[1])
	}
}

func TestParseResolv(t *testing.T) {
	input := `# Generated by NetworkManager
nameserver 1.1.1.1
nameserver 8.8.8.8
; legacy comment
search example.com
nameserver 9.9.9.9
`
	servers := parseResolv(input)
	want := []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"}
	if !reflect.DeepEqual(servers, want) {
		t.Errorf("parseResolv() = %v, want %v", servers, want)
	}
}

func TestParseResolvEmpty(t *testing.T) {
	servers := parseResolv("")
	if len(servers) != 0 {
		t.Errorf("expected empty, got %v", servers)
	}
}

func TestParseNmcliSnapshot(t *testing.T) {
	input := `ipv4.method:manual
ipv4.addresses:192.168.1.10/24
ipv4.gateway:192.168.1.1
ipv4.dns:1.1.1.1,8.8.8.8
ipv4.routes:--`

	cfg := parseNmcliSnapshot(input)
	if cfg.Method != "static" {
		t.Errorf("method: got %q, want static", cfg.Method)
	}
	if cfg.Address != "192.168.1.10" || cfg.Prefix != 24 {
		t.Errorf("address: got %s/%d", cfg.Address, cfg.Prefix)
	}
	if cfg.Gateway != "192.168.1.1" {
		t.Errorf("gateway: got %q", cfg.Gateway)
	}
	if len(cfg.DNS) != 2 || cfg.DNS[0] != "1.1.1.1" || cfg.DNS[1] != "8.8.8.8" {
		t.Errorf("dns: got %v", cfg.DNS)
	}
}

func TestParseNmcliRoutes(t *testing.T) {
	input := `dst=10.0.0.0/24, nh=192.168.1.1; dst=172.16.0.0/12, nh=10.0.0.1`
	routes := parseNmcliRoutes(input)
	if len(routes) != 2 {
		t.Fatalf("expected 2 routes, got %d", len(routes))
	}
	if routes[0].Destination != "10.0.0.0/24" || routes[0].Gateway != "192.168.1.1" {
		t.Errorf("route 0: got %v", routes[0])
	}
	if routes[1].Destination != "172.16.0.0/12" || routes[1].Gateway != "10.0.0.1" {
		t.Errorf("route 1: got %v", routes[1])
	}
}

func TestParseNmcliSnapshotDHCP(t *testing.T) {
	input := `ipv4.method:auto
ipv4.addresses:--
ipv4.gateway:--
ipv4.dns:--
ipv4.routes:--`

	cfg := parseNmcliSnapshot(input)
	if cfg.Method != "dhcp" {
		t.Errorf("method: got %q, want dhcp", cfg.Method)
	}
}

func TestParseNetworkdFile(t *testing.T) {
	input := `# Managed by nadir
[Match]
Name=eth0

[Network]
DHCP=no
Address=10.0.0.5/24
Gateway=10.0.0.1
DNS=1.1.1.1
DNS=8.8.8.8

[Route]
Destination=192.168.0.0/16
Gateway=10.0.0.254
`

	cfg := parseNetworkdFile(input)
	if cfg.Method != "static" {
		t.Errorf("method: got %q, want static", cfg.Method)
	}
	if cfg.Address != "10.0.0.5" || cfg.Prefix != 24 {
		t.Errorf("address: got %s/%d", cfg.Address, cfg.Prefix)
	}
	if cfg.Gateway != "10.0.0.1" {
		t.Errorf("gateway: got %q", cfg.Gateway)
	}
	if len(cfg.DNS) != 2 {
		t.Errorf("dns: got %v", cfg.DNS)
	}
	if len(cfg.Routes) != 1 || cfg.Routes[0].Destination != "192.168.0.0/16" || cfg.Routes[0].Gateway != "10.0.0.254" {
		t.Errorf("routes: got %v", cfg.Routes)
	}
}

func TestParseNetworkdFileDHCP(t *testing.T) {
	input := `[Match]
Name=eth0

[Network]
DHCP=yes
`
	cfg := parseNetworkdFile(input)
	if cfg.Method != "dhcp" {
		t.Errorf("method: got %q, want dhcp", cfg.Method)
	}
}

func TestParseIfupdownStanza(t *testing.T) {
	input := `# Managed by nadir
auto eth0
iface eth0 inet static
    address 192.168.1.10/24
    gateway 192.168.1.1
    dns-nameservers 1.1.1.1 8.8.8.8
    up ip route add 10.0.0.0/24 via 192.168.1.254
    down ip route del 10.0.0.0/24 via 192.168.1.254
`

	cfg := parseIfupdownStanza(input)
	if cfg.Method != "static" {
		t.Errorf("method: got %q, want static", cfg.Method)
	}
	if cfg.Address != "192.168.1.10" || cfg.Prefix != 24 {
		t.Errorf("address: got %s/%d", cfg.Address, cfg.Prefix)
	}
	if cfg.Gateway != "192.168.1.1" {
		t.Errorf("gateway: got %q", cfg.Gateway)
	}
	if len(cfg.DNS) != 2 || cfg.DNS[0] != "1.1.1.1" || cfg.DNS[1] != "8.8.8.8" {
		t.Errorf("dns: got %v", cfg.DNS)
	}
	if len(cfg.Routes) != 1 || cfg.Routes[0].Destination != "10.0.0.0/24" || cfg.Routes[0].Gateway != "192.168.1.254" {
		t.Errorf("routes: got %v", cfg.Routes)
	}
}

func TestParseIfupdownStanzaDHCP(t *testing.T) {
	input := `auto eth0
iface eth0 inet dhcp
`
	cfg := parseIfupdownStanza(input)
	if cfg.Method != "dhcp" {
		t.Errorf("method: got %q, want dhcp", cfg.Method)
	}
}

func TestRenderNetworkdFile(t *testing.T) {
	cfg := IfaceConfig{
		Method:  "static",
		Address: "10.0.0.5",
		Prefix:  24,
		Gateway: "10.0.0.1",
		DNS:     []string{"1.1.1.1"},
		Routes:  []Route{{Destination: "192.168.0.0/16", Gateway: "10.0.0.254"}},
	}
	out := renderNetworkdFile("eth0", cfg)

	mustContain := []string{
		"Name=eth0",
		"DHCP=no",
		"Address=10.0.0.5/24",
		"Gateway=10.0.0.1",
		"DNS=1.1.1.1",
		"Destination=192.168.0.0/16",
	}
	for _, s := range mustContain {
		if !strings.Contains(out, s) {
			t.Errorf("renderNetworkdFile missing %q in:\n%s", s, out)
		}
	}

	// Roundtrip test
	parsed := parseNetworkdFile(out)
	if !reflect.DeepEqual(parsed, cfg) {
		t.Errorf("roundtrip failed: got %+v, want %+v", parsed, cfg)
	}
}

func TestRenderIfupdownStanza(t *testing.T) {
	cfg := IfaceConfig{
		Method:  "static",
		Address: "192.168.1.10",
		Prefix:  24,
		Gateway: "192.168.1.1",
		DNS:     []string{"1.1.1.1", "8.8.8.8"},
		Routes:  []Route{{Destination: "10.0.0.0/24", Gateway: "192.168.1.254"}},
	}
	out := renderIfupdownStanza("eth0", cfg)

	mustContain := []string{
		"iface eth0 inet static",
		"address 192.168.1.10/24",
		"gateway 192.168.1.1",
		"dns-nameservers 1.1.1.1 8.8.8.8",
		"up ip route add 10.0.0.0/24 via 192.168.1.254",
		"down ip route del 10.0.0.0/24 via 192.168.1.254",
	}
	for _, s := range mustContain {
		if !strings.Contains(out, s) {
			t.Errorf("renderIfupdownStanza missing %q in:\n%s", s, out)
		}
	}

	// Roundtrip test
	parsed := parseIfupdownStanza(out)
	if !reflect.DeepEqual(parsed, cfg) {
		t.Errorf("roundtrip failed: got %+v, want %+v", parsed, cfg)
	}
}

func TestValidateIPv6(t *testing.T) {
	ok := IfaceConfig{Method: "dhcp", IPv6: &IPv6Config{Method: "static", Address: "2001:db8::1", Prefix: 64, Gateway: "2001:db8::ff"}}
	if err := ok.validate(); err != nil {
		t.Errorf("valid ipv6 rejected: %v", err)
	}
	bad := []IfaceConfig{
		{Method: "dhcp", IPv6: &IPv6Config{Method: "static", Address: "1.2.3.4", Prefix: 64}}, // v4 in v6 block
		{Method: "dhcp", IPv6: &IPv6Config{Method: "static", Address: "2001:db8::1", Prefix: 0}},
		{Method: "dhcp", IPv6: &IPv6Config{Method: "bogus"}},
	}
	for i, c := range bad {
		if err := c.validate(); err == nil {
			t.Errorf("bad ipv6 case %d accepted", i)
		}
	}
}

func TestParseNmcliSnapshotIPv6(t *testing.T) {
	input := `ipv4.method:manual
ipv4.addresses:192.168.1.10/24
ipv6.method:manual
ipv6.addresses:2001:db8::10/64
ipv6.gateway:2001:db8::1`
	cfg := parseNmcliSnapshot(input)
	if cfg.IPv6 == nil {
		t.Fatal("ipv6 not captured")
	}
	if cfg.IPv6.Method != "static" || cfg.IPv6.Address != "2001:db8::10" || cfg.IPv6.Prefix != 64 || cfg.IPv6.Gateway != "2001:db8::1" {
		t.Errorf("ipv6 snapshot: %+v", cfg.IPv6)
	}
}

func TestNetworkdIPv6RoundTrip(t *testing.T) {
	cfg := IfaceConfig{
		Method:  "static",
		Address: "10.0.0.5",
		Prefix:  24,
		IPv6:    &IPv6Config{Method: "static", Address: "2001:db8::5", Prefix: 64, Gateway: "2001:db8::1"},
	}
	got := parseNetworkdFile(renderNetworkdFile("eth0", cfg))
	if got.Address != "10.0.0.5" || got.Prefix != 24 {
		t.Errorf("ipv4 lost in roundtrip: %+v", got)
	}
	if !reflect.DeepEqual(got.IPv6, cfg.IPv6) {
		t.Errorf("ipv6 roundtrip: got %+v want %+v", got.IPv6, cfg.IPv6)
	}
}

func TestIfupdownIPv6RoundTrip(t *testing.T) {
	cfg := IfaceConfig{
		Method:  "static",
		Address: "192.168.1.10",
		Prefix:  24,
		IPv6:    &IPv6Config{Method: "static", Address: "2001:db8::10", Prefix: 64, Gateway: "2001:db8::1"},
	}
	got := parseIfupdownStanza(renderIfupdownStanza("eth0", cfg))
	if got.Address != "192.168.1.10" || got.Prefix != 24 || got.Method != "static" {
		t.Errorf("ipv4 lost in roundtrip: %+v", got)
	}
	if !reflect.DeepEqual(got.IPv6, cfg.IPv6) {
		t.Errorf("ipv6 roundtrip: got %+v want %+v", got.IPv6, cfg.IPv6)
	}
}

func TestNetworkdIPv6AutoIgnore(t *testing.T) {
	auto := parseNetworkdFile(renderNetworkdFile("eth0", IfaceConfig{Method: "dhcp", IPv6: &IPv6Config{Method: "auto"}}))
	if auto.IPv6 == nil || auto.IPv6.Method != "auto" {
		t.Errorf("auto roundtrip: %+v", auto.IPv6)
	}
	ign := parseNetworkdFile(renderNetworkdFile("eth0", IfaceConfig{Method: "dhcp", IPv6: &IPv6Config{Method: "ignore"}}))
	if ign.IPv6 == nil || ign.IPv6.Method != "ignore" {
		t.Errorf("ignore roundtrip: %+v", ign.IPv6)
	}
}

func TestSplitCIDR(t *testing.T) {
	tests := []struct {
		input      string
		wantAddr   string
		wantPrefix int
	}{
		{"192.168.1.10/24", "192.168.1.10", 24},
		{"10.0.0.1/8", "10.0.0.1", 8},
		{"10.0.0.1", "10.0.0.1", 0},
	}
	for _, tt := range tests {
		addr, prefix := splitCIDR(tt.input)
		if addr != tt.wantAddr || prefix != tt.wantPrefix {
			t.Errorf("splitCIDR(%q) = (%q, %d), want (%q, %d)", tt.input, addr, prefix, tt.wantAddr, tt.wantPrefix)
		}
	}
}
