package consistenthash

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/zeromicro/go-zero/core/hash"
	"google.golang.org/grpc/balancer"
	"google.golang.org/grpc/balancer/base"
	"google.golang.org/grpc/resolver"
)

type fakeSubConn struct {
	balancer.SubConn
	id int
}

func (f *fakeSubConn) Connect()                                           {}
func (f *fakeSubConn) UpdateAddresses(_ []resolver.Address)               {}
func (f *fakeSubConn) Shutdown()                                          {}
func (f *fakeSubConn) RegisterHealthListener(func(balancer.SubConnState)) {}
func (f *fakeSubConn) GetOrBuildProducer(b balancer.ProducerBuilder) (balancer.Producer, func()) {
	return nil, func() {}
}

func TestPickerBuilder_EmptyReadySCs(t *testing.T) {
	b := &pickerBuilder{}
	p := b.Build(base.PickerBuildInfo{ReadySCs: map[balancer.SubConn]base.SubConnInfo{}})

	_, err := p.Pick(balancer.PickInfo{})
	assert.Equal(t, balancer.ErrNoSubConnAvailable, err)
}

func TestPickerBuilder_BuildAndRing(t *testing.T) {
	subConn1 := &fakeSubConn{id: 1}
	subConn2 := &fakeSubConn{id: 2}
	addr1 := "127.0.0.1:8080"
	addr2 := "127.0.0.1:8081"

	b := &pickerBuilder{}
	info := base.PickerBuildInfo{
		ReadySCs: map[balancer.SubConn]base.SubConnInfo{
			subConn1: {Address: resolver.Address{Addr: addr1}},
			subConn2: {Address: resolver.Address{Addr: addr2}},
		},
	}

	p := b.Build(info).(*picker)
	assert.NotNil(t, p.hashRing)
	assert.Len(t, p.conns, 2)
}

func TestPicker_HashConsistency(t *testing.T) {
	subConn1 := &fakeSubConn{id: 1}
	subConn2 := &fakeSubConn{id: 2}

	pb := &pickerBuilder{}
	info := base.PickerBuildInfo{
		ReadySCs: map[balancer.SubConn]base.SubConnInfo{
			subConn1: {Address: resolver.Address{Addr: "127.0.0.1:8080"}},
			subConn2: {Address: resolver.Address{Addr: "127.0.0.1:8081"}},
		},
	}
	p := pb.Build(info).(*picker)
	ctx := SetHashKey(context.Background(), "user_123")
	res1, err := p.Pick(balancer.PickInfo{Ctx: ctx})
	assert.NoError(t, err)
	assert.NotNil(t, res1.SubConn)

	// Multiple requests with the same key remain consistent
	for i := 0; i < 5; i++ {
		resN, err := p.Pick(balancer.PickInfo{Ctx: ctx})
		assert.NoError(t, err)
		assert.Equal(t, res1.SubConn, resN.SubConn)
	}
}

func TestPicker_MissingKey(t *testing.T) {
	subConn := &fakeSubConn{id: 1}

	pb := &pickerBuilder{}
	info := base.PickerBuildInfo{
		ReadySCs: map[balancer.SubConn]base.SubConnInfo{
			subConn: {Address: resolver.Address{Addr: "127.0.0.1:8080"}},
		},
	}
	p := pb.Build(info).(*picker)

	// No hash key in context
	_, err := p.Pick(balancer.PickInfo{Ctx: context.Background()})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "[consistent_hash] missing hash key in context")
}

func TestPicker_NoMatchingConn(t *testing.T) {
	emptyRing := newCustomRingForTest()
	p := &picker{
		hashRing: emptyRing,
		conns:    map[string]balancer.SubConn{},
	}

	_, err := p.Pick(balancer.PickInfo{Ctx: SetHashKey(context.Background(), "someone")})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "[consistent_hash] no matching conn for hashKey: someone")
}

func TestPicker_InvalidAddrType(t *testing.T) {
	ring := newCustomRingForTest()
	ring.Add(12345)

	subConn := &fakeSubConn{id: 1}
	p := &picker{
		hashRing: ring,
		conns: map[string]balancer.SubConn{
			"12345": subConn,
		},
	}

	_, err := p.Pick(balancer.PickInfo{Ctx: SetHashKey(context.Background(), "anykey")})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "[consistent_hash] invalid addr type in consistent hash")
}

func TestPicker_NoSubConnForAddr(t *testing.T) {
	ring := newCustomRingForTest()
	ring.Add("ghost:9999")

	exist := &fakeSubConn{id: 1}
	p := &picker{
		hashRing: ring,
		conns: map[string]balancer.SubConn{
			"real:8080": exist,
		},
	}

	_, err := p.Pick(balancer.PickInfo{Ctx: SetHashKey(context.Background(), "anykey")})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "[consistent_hash] no subConn for addr: ghost:9999")
}

func TestSetAndGetHashKey(t *testing.T) {
	ctx := context.Background()
	key := "abc123"

	ctx = SetHashKey(ctx, key)
	got := GetHashKey(ctx)
	assert.Equal(t, key, got)

	assert.Empty(t, GetHashKey(context.Background()))
}

func BenchmarkPicker_HashConsistency(b *testing.B) {
	subConn1 := &fakeSubConn{id: 1}
	subConn2 := &fakeSubConn{id: 2}

	pb := &pickerBuilder{}
	info := base.PickerBuildInfo{
		ReadySCs: map[balancer.SubConn]base.SubConnInfo{
			subConn1: {Address: resolver.Address{Addr: "127.0.0.1:8080"}},
			subConn2: {Address: resolver.Address{Addr: "127.0.0.1:8081"}},
		},
	}
	p := pb.Build(info).(*picker)

	ctx := SetHashKey(context.Background(), "hot_user_123")

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		res, err := p.Pick(balancer.PickInfo{Ctx: ctx})
		if err != nil || res.SubConn == nil {
			b.Fatalf("unexpected result: res=%v err=%v", res.SubConn, err)
		}
	}
}

func newCustomRingForTest() *hash.ConsistentHash {
	return hash.NewCustomConsistentHash(defaultReplicaCount, hash.Hash)
}
