package rdns_test

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

	"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
	"github.com/AdguardTeam/AdGuardHome/internal/rdns"
	"github.com/AdguardTeam/golibs/netutil"
	"github.com/AdguardTeam/golibs/testutil"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// testTimeout is a common timeout for tests and contexts.
const testTimeout = 1 * time.Second

func TestDefault_Process(t *testing.T) {
	ip1 := netip.MustParseAddr("1.2.3.4")
	revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
	require.NoError(t, err)

	ip2 := netip.MustParseAddr("4.3.2.1")
	revAddr2, err := netutil.IPToReversedAddr(ip2.AsSlice())
	require.NoError(t, err)

	localIP := netip.MustParseAddr("192.168.0.1")
	localRevAddr1, err := netutil.IPToReversedAddr(localIP.AsSlice())
	require.NoError(t, err)

	testCases := []struct {
		name string
		addr netip.Addr
		want string
	}{{
		name: "first",
		addr: ip1,
		want: revAddr1,
	}, {
		name: "second",
		addr: ip2,
		want: revAddr2,
	}, {
		name: "empty",
		addr: netip.MustParseAddr("0.0.0.0"),
		want: "",
	}, {
		name: "private",
		addr: localIP,
		want: localRevAddr1,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			hit := 0
			onExchange := func(
				_ context.Context,
				ip netip.Addr,
			) (host string, ttl time.Duration, err error) {
				hit++

				switch ip {
				case ip1:
					return revAddr1, time.Hour, nil
				case ip2:
					return revAddr2, time.Hour, nil
				case localIP:
					return localRevAddr1, time.Hour, nil
				default:
					return "", time.Hour, nil
				}
			}

			r := rdns.New(&rdns.Config{
				CacheSize: 100,
				CacheTTL:  time.Hour,
				Exchanger: &aghtest.Exchanger{OnExchange: onExchange},
			})

			got, changed := r.Process(testutil.ContextWithTimeout(t, testTimeout), tc.addr)
			require.True(t, changed)

			assert.Equal(t, tc.want, got)
			assert.Equal(t, 1, hit)

			// From cache.
			got, changed = r.Process(testutil.ContextWithTimeout(t, testTimeout), tc.addr)
			require.False(t, changed)

			assert.Equal(t, tc.want, got)
			assert.Equal(t, 1, hit)
		})
	}

	t.Run("zero_ttl", func(t *testing.T) {
		const cacheTTL = time.Second / 2

		zeroTTLExchanger := &aghtest.Exchanger{
			OnExchange: func(
				_ context.Context,
				ip netip.Addr,
			) (host string, ttl time.Duration, err error) {
				return revAddr1, 0, nil
			},
		}

		r := rdns.New(&rdns.Config{
			CacheSize: 1,
			CacheTTL:  cacheTTL,
			Exchanger: zeroTTLExchanger,
		})

		got, changed := r.Process(testutil.ContextWithTimeout(t, testTimeout), ip1)
		require.True(t, changed)
		assert.Equal(t, revAddr1, got)

		zeroTTLExchanger.OnExchange = func(
			_ context.Context,
			ip netip.Addr,
		) (host string, ttl time.Duration, err error) {
			return revAddr2, time.Hour, nil
		}

		ctx := testutil.ContextWithTimeout(t, testTimeout)
		require.EventuallyWithT(t, func(t *assert.CollectT) {
			got, changed = r.Process(ctx, ip1)
			assert.True(t, changed)
			assert.Equal(t, revAddr2, got)
		}, 2*cacheTTL, time.Millisecond*100)

		assert.Never(t, func() (changed bool) {
			_, changed = r.Process(testutil.ContextWithTimeout(t, testTimeout), ip1)

			return changed
		}, 2*cacheTTL, time.Millisecond*100)
	})
}
