package libnetwork

import (
	"fmt"
	"net"
	"net/netip"
	"slices"
	"testing"

	"github.com/gogo/protobuf/proto"
	"gotest.tools/v3/assert"

	"github.com/moby/moby/v2/daemon/libnetwork/networkdb"
)

func TestEndpointEvent_EquivalentTo(t *testing.T) {
	assert.Check(t, (&endpointEvent{}).EquivalentTo(&endpointEvent{}))

	a := endpointEvent{
		EndpointRecord: EndpointRecord{
			Name:        "foo",
			ServiceName: "bar",
			ServiceID:   "baz",
			IngressPorts: []*PortConfig{
				{
					Protocol:   ProtocolTCP,
					TargetPort: 80,
				},
				{
					Name:          "dns",
					Protocol:      ProtocolUDP,
					TargetPort:    5353,
					PublishedPort: 53,
				},
			},
		},
		VirtualIP:  netip.MustParseAddr("10.0.0.42"),
		EndpointIP: netip.MustParseAddr("192.168.69.42"),
	}
	assert.Check(t, a.EquivalentTo(&a))

	reflexiveEquiv := func(a, b *endpointEvent) bool {
		t.Helper()
		assert.Check(t, a.EquivalentTo(b) == b.EquivalentTo(a), "reflexive equivalence")
		return a.EquivalentTo(b)
	}

	assert.Check(t, reflexiveEquiv(nil, nil), "nil should be equivalent to nil")
	assert.Check(t, !reflexiveEquiv(&a, nil), "non-nil should not be equivalent to nil")

	b := a
	b.ServiceDisabled = true
	assert.Check(t, reflexiveEquiv(&a, &b), "ServiceDisabled value should not matter")

	c := a
	c.IngressPorts = slices.Clone(a.IngressPorts)
	slices.Reverse(c.IngressPorts)
	assert.Check(t, reflexiveEquiv(&a, &c), "IngressPorts order should not matter")

	d := a
	d.IngressPorts = append(d.IngressPorts, a.IngressPorts[0])
	assert.Check(t, !reflexiveEquiv(&a, &d), "Differing number of copies of IngressPort entries should not be equivalent")
	d.IngressPorts = a.IngressPorts[:1]
	assert.Check(t, !reflexiveEquiv(&a, &d), "Removing an IngressPort entry should not be equivalent")

	e := a
	e.Aliases = []string{"alias1", "alias2"}
	assert.Check(t, !reflexiveEquiv(&a, &e), "Differing Aliases should not be equivalent")

	f := a
	f.TaskAliases = []string{"taskalias1", "taskalias2"}
	assert.Check(t, !reflexiveEquiv(&a, &f), "Adding TaskAliases should not be equivalent")
	g := a
	g.TaskAliases = []string{"taskalias2", "taskalias1"}
	assert.Check(t, reflexiveEquiv(&f, &g), "TaskAliases order should not matter")
	g.TaskAliases = g.TaskAliases[:1]
	assert.Check(t, !reflexiveEquiv(&f, &g), "Differing number of TaskAliases should not be equivalent")

	h := a
	h.EndpointIP = netip.MustParseAddr("192.168.69.43")
	assert.Check(t, !reflexiveEquiv(&a, &h), "Differing EndpointIP should not be equivalent")

	i := a
	i.VirtualIP = netip.MustParseAddr("10.0.0.69")
	assert.Check(t, !reflexiveEquiv(&a, &i), "Differing VirtualIP should not be equivalent")

	j := a
	j.ServiceID = "qux"
	assert.Check(t, !reflexiveEquiv(&a, &j), "Differing ServiceID should not be equivalent")

	k := a
	k.ServiceName = "quux"
	assert.Check(t, !reflexiveEquiv(&a, &k), "Differing ServiceName should not be equivalent")

	l := a
	l.Name = "aaaaa"
	assert.Check(t, !reflexiveEquiv(&a, &l), "Differing Name should not be equivalent")
}

type mockServiceBinder struct {
	actions []string
}

func (m *mockServiceBinder) addContainerNameResolution(nID, eID, containerName string, _ []string, ip net.IP, _ string) error {
	m.actions = append(m.actions, fmt.Sprintf("addContainerNameResolution(%v, %v, %v, %v)", nID, eID, containerName, ip))
	return nil
}

func (m *mockServiceBinder) delContainerNameResolution(nID, eID, containerName string, _ []string, ip net.IP, _ string) error {
	m.actions = append(m.actions, fmt.Sprintf("delContainerNameResolution(%v, %v, %v, %v)", nID, eID, containerName, ip))
	return nil
}

func (m *mockServiceBinder) addServiceBinding(svcName, svcID, nID, eID, containerName string, vip net.IP, _ []*PortConfig, _, _ []string, ip net.IP, _ string) error {
	m.actions = append(m.actions, fmt.Sprintf("addServiceBinding(%v, %v, %v, %v, %v, %v, %v)", svcName, svcID, nID, eID, containerName, vip, ip))
	return nil
}

func (m *mockServiceBinder) rmServiceBinding(svcName, svcID, nID, eID, containerName string, vip net.IP, _ []*PortConfig, _, _ []string, ip net.IP, _ string, deleteSvcRecords bool, fullRemove bool) error {
	m.actions = append(m.actions, fmt.Sprintf("rmServiceBinding(%v, %v, %v, %v, %v, %v, %v, deleteSvcRecords=%v, fullRemove=%v)", svcName, svcID, nID, eID, containerName, vip, ip, deleteSvcRecords, fullRemove))
	return nil
}

func TestHandleEPTableEvent(t *testing.T) {
	svc1 := EndpointRecord{
		Name:        "ep1",
		ServiceName: "svc1",
		ServiceID:   "id1",
		VirtualIP:   "10.0.0.1",
		EndpointIP:  "192.168.12.42",
	}
	svc1disabled := svc1
	svc1disabled.ServiceDisabled = true

	svc2 := EndpointRecord{
		Name:        "ep2",
		ServiceName: "svc2",
		ServiceID:   "id2",
		VirtualIP:   "10.0.0.2",
		EndpointIP:  "172.16.69.5",
	}
	svc2disabled := svc2
	svc2disabled.ServiceDisabled = true

	ctr1 := EndpointRecord{
		Name:       "ctr1",
		EndpointIP: "172.18.1.1",
	}
	ctr1disabled := ctr1
	ctr1disabled.ServiceDisabled = true

	ctr2 := EndpointRecord{
		Name:       "ctr2",
		EndpointIP: "172.18.1.2",
	}
	ctr2disabled := ctr2
	ctr2disabled.ServiceDisabled = true

	tests := []struct {
		name            string
		prev, ev        *EndpointRecord
		expectedActions []string
	}{
		{
			name: "Insert/Service/ServiceDisabled=false",
			ev:   &svc1,
			expectedActions: []string{
				"addServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42)",
			},
		},
		{
			name: "Insert/Service/ServiceDisabled=true",
			ev:   &svc1disabled,
		},
		{
			name: "Insert/Container/ServiceDisabled=false",
			ev:   &ctr1,
			expectedActions: []string{
				"addContainerNameResolution(network1, endpoint1, ctr1, 172.18.1.1)",
			},
		},
		{
			name: "Insert/Container/ServiceDisabled=true",
			ev:   &ctr1disabled,
		},

		{
			name: "Update/Service/ServiceDisabled=ft",
			prev: &svc1,
			ev:   &svc1disabled,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=false)",
			},
		},
		{
			name: "Update/Service/ServiceDisabled=tf",
			prev: &svc1disabled,
			ev:   &svc1,
			expectedActions: []string{
				"addServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42)",
			},
		},
		{
			name: "Update/Service/ServiceDisabled=ff",
			prev: &svc1disabled,
			ev:   &svc1disabled,
		},
		{
			name: "Update/Service/ServiceDisabled=tt",
			prev: &svc1,
			ev:   &svc1,
		},
		{
			name: "Update/Container/ServiceDisabled=ft",
			prev: &ctr1,
			ev:   &ctr1disabled,
			expectedActions: []string{
				"delContainerNameResolution(network1, endpoint1, ctr1, 172.18.1.1)",
			},
		},
		{
			name: "Update/Container/ServiceDisabled=tf",
			prev: &ctr1disabled,
			ev:   &ctr1,
			expectedActions: []string{
				"addContainerNameResolution(network1, endpoint1, ctr1, 172.18.1.1)",
			},
		},
		{
			name: "Update/Container/ServiceDisabled=ff",
			prev: &ctr1disabled,
			ev:   &ctr1disabled,
		},
		{
			name: "Update/Container/ServiceDisabled=tt",
			prev: &ctr1,
			ev:   &ctr1,
		},

		{
			name: "Delete/Service/ServiceDisabled=false",
			prev: &svc1,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
			},
		},
		{
			name: "Delete/Service/ServiceDisabled=true",
			prev: &svc1disabled,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
			},
		},

		{
			name: "Delete/Container/ServiceDisabled=false",
			prev: &ctr1,
			expectedActions: []string{
				"delContainerNameResolution(network1, endpoint1, ctr1, 172.18.1.1)",
			},
		},
		{
			name: "Delete/Container/ServiceDisabled=true",
			prev: &ctr1disabled,
		},

		{
			name: "Replace/From=Service/To=Service/ServiceDisabled=ff",
			prev: &svc1,
			ev:   &svc2,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
				"addServiceBinding(svc2, id2, network1, endpoint1, ep2, 10.0.0.2, 172.16.69.5)",
			},
		},
		{
			name: "Replace/From=Service/To=Service/ServiceDisabled=ft",
			prev: &svc1,
			ev:   &svc2disabled,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
			},
		},
		{
			name: "Replace/From=Service/To=Service/ServiceDisabled=tf",
			prev: &svc1disabled,
			ev:   &svc2,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
				"addServiceBinding(svc2, id2, network1, endpoint1, ep2, 10.0.0.2, 172.16.69.5)",
			},
		},
		{
			name: "Replace/From=Service/To=Service/ServiceDisabled=tt",
			prev: &svc1disabled,
			ev:   &svc2disabled,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
			},
		},
		{
			name: "Replace/From=Service/To=Container/ServiceDisabled=ff",
			prev: &svc1,
			ev:   &ctr2,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
				"addContainerNameResolution(network1, endpoint1, ctr2, 172.18.1.2)",
			},
		},
		{
			name: "Replace/From=Service/To=Container/ServiceDisabled=ft",
			prev: &svc1,
			ev:   &ctr2disabled,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
			},
		},
		{
			name: "Replace/From=Service/To=Container/ServiceDisabled=tf",
			prev: &svc1disabled,
			ev:   &ctr2,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
				"addContainerNameResolution(network1, endpoint1, ctr2, 172.18.1.2)",
			},
		},
		{
			name: "Replace/From=Service/To=Container/ServiceDisabled=tt",
			prev: &svc1disabled,
			ev:   &ctr2disabled,
			expectedActions: []string{
				"rmServiceBinding(svc1, id1, network1, endpoint1, ep1, 10.0.0.1, 192.168.12.42, deleteSvcRecords=true, fullRemove=true)",
			},
		},
		{
			name: "Replace/From=Container/To=Service/ServiceDisabled=ff",
			prev: &ctr1,
			ev:   &svc2,
			expectedActions: []string{
				"delContainerNameResolution(network1, endpoint1, ctr1, 172.18.1.1)",
				"addServiceBinding(svc2, id2, network1, endpoint1, ep2, 10.0.0.2, 172.16.69.5)",
			},
		},
		{
			name: "Replace/From=Container/To=Service/ServiceDisabled=ft",
			prev: &ctr1,
			ev:   &svc2disabled,
			expectedActions: []string{
				"delContainerNameResolution(network1, endpoint1, ctr1, 172.18.1.1)",
			},
		},
		{
			name: "Replace/From=Container/To=Service/ServiceDisabled=tf",
			prev: &ctr1disabled,
			ev:   &svc2,
			expectedActions: []string{
				"addServiceBinding(svc2, id2, network1, endpoint1, ep2, 10.0.0.2, 172.16.69.5)",
			},
		},
		{
			name: "Replace/From=Container/To=Service/ServiceDisabled=tt",
			prev: &ctr1disabled,
			ev:   &svc2disabled,
		},
		{
			name: "Replace/From=Container/To=Container/ServiceDisabled=ff",
			prev: &ctr1,
			ev:   &ctr2,
			expectedActions: []string{
				"delContainerNameResolution(network1, endpoint1, ctr1, 172.18.1.1)",
				"addContainerNameResolution(network1, endpoint1, ctr2, 172.18.1.2)",
			},
		},
		{
			name: "Replace/From=Container/To=Container/ServiceDisabled=ft",
			prev: &ctr1,
			ev:   &ctr2disabled,
			expectedActions: []string{
				"delContainerNameResolution(network1, endpoint1, ctr1, 172.18.1.1)",
			},
		},
		{
			name: "Replace/From=Container/To=Container/ServiceDisabled=tf",
			prev: &ctr1disabled,
			ev:   &ctr2,
			expectedActions: []string{
				"addContainerNameResolution(network1, endpoint1, ctr2, 172.18.1.2)",
			},
		},
		{
			name: "Replace/From=Container/To=Container/ServiceDisabled=tt",
			prev: &ctr1disabled,
			ev:   &ctr2disabled,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			msb := &mockServiceBinder{}
			event := networkdb.WatchEvent{
				NetworkID: "network1",
				Key:       "endpoint1",
			}
			var err error
			if tt.prev != nil {
				event.Prev, err = proto.Marshal(tt.prev)
				assert.NilError(t, err)
			}
			if tt.ev != nil {
				event.Value, err = proto.Marshal(tt.ev)
				assert.NilError(t, err)
			}
			handleEpTableEvent(msb, event)
			assert.DeepEqual(t, tt.expectedActions, msb.actions)
		})
	}
}