//go:build darwin || freebsd || linux || openbsd

package dhcpd

import (
	"net"
	"net/netip"
	"os"
	"testing"
	"time"

	"github.com/AdguardTeam/golibs/testutil"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/slices"
)

func TestMain(m *testing.M) {
	testutil.DiscardLogOutput(m)
}

func testNotify(flags uint32) {
}

// Leases database store/load.
func TestDB(t *testing.T) {
	var err error
	s := server{
		conf: &ServerConfig{
			DBFilePath: dbFilename,
		},
	}

	s.srv4, err = v4Create(&V4ServerConf{
		Enabled:    true,
		RangeStart: netip.MustParseAddr("192.168.10.100"),
		RangeEnd:   netip.MustParseAddr("192.168.10.200"),
		GatewayIP:  netip.MustParseAddr("192.168.10.1"),
		SubnetMask: netip.MustParseAddr("255.255.255.0"),
		notify:     testNotify,
	})
	require.NoError(t, err)

	s.srv6, err = v6Create(V6ServerConf{})
	require.NoError(t, err)

	leases := []*Lease{{
		Expiry:   time.Now().Add(time.Hour),
		Hostname: "static-1.local",
		HWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
		IP:       net.IP{192, 168, 10, 100},
	}, {
		Hostname: "static-2.local",
		HWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB},
		IP:       net.IP{192, 168, 10, 101},
	}}

	srv4, ok := s.srv4.(*v4Server)
	require.True(t, ok)

	err = srv4.addLease(leases[0])
	require.NoError(t, err)

	err = s.srv4.AddStaticLease(leases[1])
	require.NoError(t, err)

	err = s.dbStore()
	require.NoError(t, err)

	testutil.CleanupAndRequireSuccess(t, func() (err error) { return os.Remove(dbFilename) })

	err = s.srv4.ResetLeases(nil)
	require.NoError(t, err)

	err = s.dbLoad()
	require.NoError(t, err)

	ll := s.srv4.GetLeases(LeasesAll)
	require.Len(t, ll, len(leases))

	assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr)
	assert.Equal(t, leases[1].IP, ll[0].IP)
	assert.True(t, ll[0].IsStatic())

	assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr)
	assert.Equal(t, leases[0].IP, ll[1].IP)
	assert.Equal(t, leases[0].Expiry.Unix(), ll[1].Expiry.Unix())
}

func TestNormalizeLeases(t *testing.T) {
	dynLeases := []*Lease{{
		HWAddr: net.HardwareAddr{1, 2, 3, 4},
	}, {
		HWAddr: net.HardwareAddr{1, 2, 3, 5},
	}}

	staticLeases := []*Lease{{
		HWAddr: net.HardwareAddr{1, 2, 3, 4},
		IP:     net.IP{0, 2, 3, 4},
	}, {
		HWAddr: net.HardwareAddr{2, 2, 3, 4},
	}}

	leases := normalizeLeases(staticLeases, dynLeases)
	require.Len(t, leases, 3)

	assert.Equal(t, leases[0].HWAddr, dynLeases[0].HWAddr)
	assert.Equal(t, leases[0].IP, staticLeases[0].IP)
	assert.Equal(t, leases[1].HWAddr, staticLeases[1].HWAddr)
	assert.Equal(t, leases[2].HWAddr, dynLeases[1].HWAddr)
}

func TestV4Server_badRange(t *testing.T) {
	testCases := []struct {
		name       string
		gatewayIP  netip.Addr
		subnetMask netip.Addr
		wantErrMsg string
	}{{
		name:       "gateway_in_range",
		gatewayIP:  netip.MustParseAddr("192.168.10.120"),
		subnetMask: netip.MustParseAddr("255.255.255.0"),
		wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " +
			"192.168.10.20-192.168.10.200",
	}, {
		name:       "outside_range_start",
		gatewayIP:  netip.MustParseAddr("192.168.10.1"),
		subnetMask: netip.MustParseAddr("255.255.255.240"),
		wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " +
			"192.168.10.1/28",
	}, {
		name:       "outside_range_end",
		gatewayIP:  netip.MustParseAddr("192.168.10.1"),
		subnetMask: netip.MustParseAddr("255.255.255.224"),
		wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " +
			"192.168.10.1/27",
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			conf := V4ServerConf{
				Enabled:    true,
				RangeStart: netip.MustParseAddr("192.168.10.20"),
				RangeEnd:   netip.MustParseAddr("192.168.10.200"),
				GatewayIP:  tc.gatewayIP,
				SubnetMask: tc.subnetMask,
				notify:     testNotify,
			}

			_, err := v4Create(&conf)
			testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
		})
	}
}

// cloneUDPAddr returns a deep copy of a.
func cloneUDPAddr(a *net.UDPAddr) (clone *net.UDPAddr) {
	return &net.UDPAddr{
		IP:   slices.Clone(a.IP),
		Port: a.Port,
		Zone: a.Zone,
	}
}
