package peer

import (
	"context"
	"strconv"
	"testing"
	"time"

	"github.com/go-faster/errors"
	"github.com/stretchr/testify/require"

	"github.com/gotd/neo"

	"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)

func TestLRU(t *testing.T) {
	a := require.New(t)
	c := neo.NewTime(time.Now())
	lru := NewLRUResolver(nil, 4).WithClock(c).WithExpiration(1 * time.Second)

	// Add 5 entries.
	// State: [4 3 2 1]
	for i := range [5]struct{}{} {
		lru.put(strconv.Itoa(i), &tg.InputPeerChat{
			ChatID: int64(i),
		})
	}

	// First entry must be evicted.
	// State: [4 3 2 1]
	_, ok := lru.get(strconv.Itoa(0))
	a.False(ok)

	// Third must not.
	// State: [2 3 4 1]
	_, ok = lru.get(strconv.Itoa(2))
	a.True(ok)
	a.Equal("2", lru.lruList.Front().nodeData.key)

	// Add yet another.
	// State: [6 2 3 4]
	lru.put(strconv.Itoa(6), &tg.InputPeerChat{
		ChatID: 6,
	})
	a.Equal("6", lru.lruList.Front().nodeData.key)

	// Then yet one must be evicted.
	_, ok = lru.get(strconv.Itoa(1))
	a.False(ok)

	// Add which already exist.
	// State: [4 6 2 3]
	lru.put(strconv.Itoa(4), &tg.InputPeerChat{
		ChatID: 6,
	})
	a.Equal("4", lru.lruList.Front().nodeData.key)

	// Delete key which does not exist.
	// State: [4 6 2 3]
	_, ok = lru.Evict(strconv.Itoa(10))
	a.False(ok)

	c.Travel(time.Hour)
	// Delete expired key.
	// State: [6 2 3]
	_, ok = lru.get("4")
	a.False(ok)
}

type mockResolver struct {
	counter       int
	returnErr     bool
	domain, phone string
	peer          tg.InputPeerClass
	t             testing.TB
}

func (m *mockResolver) ResolveDomain(ctx context.Context, domain string) (tg.InputPeerClass, error) {
	m.counter++

	if m.returnErr && m.counter == 1 {
		return nil, testutil.TestError()
	}

	if domain != m.domain {
		err := errors.Errorf("expected domain %q, got %q", m.domain, domain)
		m.t.Error(err)
		return nil, err
	}
	return m.peer, nil
}

func (m *mockResolver) ResolvePhone(ctx context.Context, phone string) (tg.InputPeerClass, error) {
	m.counter++

	if m.returnErr && m.counter == 1 {
		return nil, testutil.TestError()
	}

	if phone != m.phone {
		err := errors.Errorf("expected phone %q, got %q", m.phone, phone)
		m.t.Error(err)
		return nil, err
	}
	return m.peer, nil
}

func TestLRUResolver_Resolve(t *testing.T) {
	ctx := context.Background()
	expectedDomain := "telegram"
	expected := &tg.InputPeerUser{
		UserID: 10,
	}

	t.Run("Cache", func(t *testing.T) {
		a := require.New(t)
		resolver := &mockResolver{
			domain: expectedDomain,
			peer:   expected,
			t:      t,
		}

		lru := NewLRUResolver(resolver, 10)

		r, err := lru.ResolveDomain(ctx, expectedDomain)
		a.NoError(err)
		a.Equal(expected, r)

		r2, err := lru.ResolveDomain(ctx, expectedDomain)
		a.NoError(err)
		a.Equal(expected, r2)

		a.Equalf(1, resolver.counter, "RPC call was not cached")
	})

	t.Run("Error", func(t *testing.T) {
		a := require.New(t)
		resolver := &mockResolver{
			returnErr: true,
			domain:    expectedDomain,
			peer:      expected,
			t:         t,
		}

		lru := NewLRUResolver(resolver, 10)

		_, err := lru.ResolveDomain(ctx, expectedDomain)
		a.Error(err)

		r2, err := lru.ResolveDomain(ctx, expectedDomain)
		a.NoError(err)
		a.Equal(expected, r2)

		a.Equalf(2, resolver.counter, "RPC call error was cached")
	})
}
