package netstack

import (
	"context"
	"net"
	"reflect"
	"strings"
	"testing"

	cnilibrary "github.com/containernetworking/cni/libcni"
	cnitypes "github.com/containernetworking/cni/pkg/types"
	types100 "github.com/containernetworking/cni/pkg/types/100"
)

func TestNormalizeCNIManagerConfigDefaults(t *testing.T) {
	cfg := normalizeCNIManagerConfig(CNIManagerConfig{})

	if !reflect.DeepEqual(cfg.PluginBinDirs, []string{defaultCNIPluginBinDir}) {
		t.Fatalf("PluginBinDirs = %v, want [%s]", cfg.PluginBinDirs, defaultCNIPluginBinDir)
	}
	if cfg.PluginConfDir != defaultCNIPluginConfDir {
		t.Fatalf("PluginConfDir = %q, want %q", cfg.PluginConfDir, defaultCNIPluginConfDir)
	}
	if cfg.CacheDir != defaultCNICacheDir {
		t.Fatalf("CacheDir = %q, want %q", cfg.CacheDir, defaultCNICacheDir)
	}
}

func TestExtractCNIDNS(t *testing.T) {
	result := &types100.Result{DNS: cnitypes.DNS{
		Nameservers: []string{"10.0.0.53", "10.0.0.54"},
		Search:      []string{"one.example", "two.example"},
		Options:     []string{"timeout:2"},
		Domain:      "ignored.example",
	}}
	got, err := extractCNIDNS(result)
	if err != nil {
		t.Fatalf("extractCNIDNS() error = %v", err)
	}
	if !reflect.DeepEqual(got.Nameservers, []string{"10.0.0.53", "10.0.0.54"}) ||
		!reflect.DeepEqual(got.Search, []string{"one.example", "two.example"}) ||
		got.Domain != "" {
		t.Fatalf("extractCNIDNS() = %#v", got)
	}
}

func TestExtractCNIDNSRejectsInvalidExplicitServer(t *testing.T) {
	result := &types100.Result{DNS: cnitypes.DNS{Nameservers: []string{"127.0.0.53"}}}
	if _, err := extractCNIDNS(result); err == nil {
		t.Fatal("extractCNIDNS() error = nil, want invalid CNI DNS error")
	}
}

func TestValidateCNIIPv4OnlyRejectsIPv6(t *testing.T) {
	_, ipv6Route, _ := net.ParseCIDR("fd00::/64")
	_, ipv4Route, _ := net.ParseCIDR("0.0.0.0/0")
	tests := []struct {
		name   string
		result *types100.Result
		want   string
	}{
		{name: "address", result: &types100.Result{IPs: []*types100.IPConfig{{Address: net.IPNet{IP: net.ParseIP("fd00::2")}}}}, want: "IPv6 address"},
		{name: "gateway", result: &types100.Result{IPs: []*types100.IPConfig{{Gateway: net.ParseIP("fd00::1")}}}, want: "IPv6 gateway"},
		{name: "route", result: &types100.Result{Routes: []*cnitypes.Route{{Dst: *ipv6Route}}}, want: "IPv6 route"},
		{name: "route gateway", result: &types100.Result{Routes: []*cnitypes.Route{{Dst: *ipv4Route, GW: net.ParseIP("fd00::1")}}}, want: "IPv6 route gateway"},
		{name: "DNS", result: &types100.Result{DNS: cnitypes.DNS{Nameservers: []string{"2001:4860:4860::8888"}}}, want: "IPv6 DNS"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := validateCNIIPv4Only(tt.result); err == nil || !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("validateCNIIPv4Only() error = %v, want substring %q", err, tt.want)
			}
		})
	}

	if err := validateCNIIPv4Only(&types100.Result{
		IPs:    []*types100.IPConfig{{Address: net.IPNet{IP: net.ParseIP("10.12.0.2")}, Gateway: net.ParseIP("10.12.0.1")}},
		Routes: []*cnitypes.Route{{Dst: *ipv4Route, GW: net.ParseIP("10.12.0.1")}},
		DNS:    cnitypes.DNS{Nameservers: []string{"8.8.8.8"}},
	}); err != nil {
		t.Fatalf("validateCNIIPv4Only() rejected IPv4 result: %v", err)
	}
}

func TestNormalizeCNIManagerConfigPreservesExplicitValues(t *testing.T) {
	in := CNIManagerConfig{
		PluginBinDirs: []string{"/custom/bin"},
		PluginConfDir: "/custom/net.d",
		CacheDir:      "/custom/cache",
	}
	got := normalizeCNIManagerConfig(in)

	if !reflect.DeepEqual(got, in) {
		t.Fatalf("normalizeCNIManagerConfig() = %#v, want %#v", got, in)
	}
}

func TestLoadedBridgeNetwork(t *testing.T) {
	tests := []struct {
		name        string
		config      *cnilibrary.NetworkConfigList
		wantNetwork string
		wantBridge  string
		wantErr     string
	}{
		{
			name: "reads loaded bridge plugin",
			config: &cnilibrary.NetworkConfigList{
				Name: "custom-network",
				Plugins: []*cnilibrary.PluginConfig{{
					Network: &cnitypes.PluginConf{Type: "bridge"},
					Bytes:   []byte(`{"bridge":"custom-bridge"}`),
				}},
			},
			wantNetwork: "custom-network",
			wantBridge:  "custom-bridge",
		},
		{
			name:    "rejects missing configuration",
			config:  nil,
			wantErr: "no loaded configuration",
		},
		{
			name: "rejects missing bridge plugin",
			config: &cnilibrary.NetworkConfigList{
				Name:    "custom-network",
				Plugins: []*cnilibrary.PluginConfig{{Network: &cnitypes.PluginConf{Type: "host-local"}}},
			},
			wantErr: "no bridge network",
		},
		{
			name: "rejects missing bridge name",
			config: &cnilibrary.NetworkConfigList{
				Name: "custom-network",
				Plugins: []*cnilibrary.PluginConfig{{
					Network: &cnitypes.PluginConf{Type: "bridge"},
					Bytes:   []byte(`{}`),
				}},
			},
			wantErr: "has no bridge name",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotNetwork, gotBridge, err := loadedBridgeNetwork(tt.config)
			if tt.wantErr != "" {
				if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
					t.Fatalf("loadedBridgeNetwork() error = %v, want substring %q", err, tt.wantErr)
				}
				return
			}
			if err != nil {
				t.Fatalf("loadedBridgeNetwork() error = %v", err)
			}
			if gotNetwork != tt.wantNetwork || gotBridge != tt.wantBridge {
				t.Fatalf("loadedBridgeNetwork() = (%q, %q), want (%q, %q)", gotNetwork, gotBridge, tt.wantNetwork, tt.wantBridge)
			}
		})
	}
}

func TestExtractCNIIP(t *testing.T) {
	result := &types100.Result{
		Interfaces: []*types100.Interface{{Name: "eth0"}, {Name: "host"}},
		IPs: []*types100.IPConfig{
			{Interface: types100.Int(0), Address: net.IPNet{IP: net.ParseIP("10.12.0.2")}},
		},
	}

	got, err := extractCNIIP(result)
	if err != nil {
		t.Fatalf("extractCNIIP() error = %v", err)
	}
	if got != "10.12.0.2" {
		t.Fatalf("IP = %q, want 10.12.0.2", got)
	}
}

func TestExtractCNIIPRejectsOtherInterface(t *testing.T) {
	result := &types100.Result{
		Interfaces: []*types100.Interface{{Name: "net1"}},
		IPs:        []*types100.IPConfig{{Interface: types100.Int(0), Address: net.IPNet{IP: net.ParseIP("10.12.0.8")}}},
	}
	if _, err := extractCNIIP(result); err == nil {
		t.Fatal("extractCNIIP(other interface) error = nil, want error")
	}
}

func TestExtractCNIIPRejectsInvalidResults(t *testing.T) {
	if _, err := extractCNIIP(nil); err == nil {
		t.Fatal("extractCNIIP(nil) error = nil, want error")
	}
	if _, err := extractCNIIP(&types100.Result{Interfaces: []*types100.Interface{{Name: cniOuterInterfaceName}}}); err == nil {
		t.Fatal("extractCNIIP(empty interface) error = nil, want error")
	}
	if _, err := extractCNIIP(&types100.Result{
		Interfaces: []*types100.Interface{{Name: cniOuterInterfaceName}},
		IPs:        []*types100.IPConfig{nil, {}},
	}); err == nil {
		t.Fatal("extractCNIIP(nil IP configs) error = nil, want error")
	}
	if _, err := extractCNIIP(&types100.Result{
		Interfaces: []*types100.Interface{{Name: cniOuterInterfaceName}},
		IPs:        []*types100.IPConfig{{Interface: types100.Int(1), Address: net.IPNet{IP: net.ParseIP("10.12.0.2")}}},
	}); err == nil {
		t.Fatal("extractCNIIP(invalid interface) error = nil, want error")
	}
}

func TestCNIManagerDelegatesTeardown(t *testing.T) {
	var removeID, removePath string
	manager := &CNIManager{backend: &fakeCNIBackend{
		remove: func(_ context.Context, id, path string) error {
			removeID, removePath = id, path
			return nil
		},
	}}

	if err := manager.TeardownSandboxNetwork(context.Background(), "slot-2", "/run/conch/netns/slot-2"); err != nil {
		t.Fatalf("TeardownSandboxNetwork(): %v", err)
	}
	if removeID != "slot-2" || removePath != "/run/conch/netns/slot-2" {
		t.Fatalf("Remove identity = (%q, %q), want slot identity and netns path", removeID, removePath)
	}
}