package proxy

import (
	"context"
	"errors"
	"testing"

	"github.com/siderolabs/grpc-proxy/proxy"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

type mockMapper struct {
	targets []MachineTarget
	err     error
}

func (m *mockMapper) MapMachines(_ context.Context, _ []string) ([]MachineTarget, error) {
	if m.err != nil {
		return nil, m.err
	}
	return m.targets, nil
}

func TestDirector_Director(t *testing.T) {
	d := NewDirector("/tmp/test.sock", 8080, nil)
	t.Cleanup(d.Close)

	// Use a valid IPv6 address for remote targets.
	remoteTarget := MachineTarget{ID: "id-2", Name: "machine-b", Addr: "fd00::2"}
	localTarget := MachineTarget{ID: "id-1", Name: "machine-a", Addr: "fd00::1"}

	t.Run("no metadata routes to local", func(t *testing.T) {
		ctx := context.Background()
		mode, backends, err := d.Director(ctx, "/Test/Method")

		require.NoError(t, err)
		assert.Equal(t, proxy.One2One, mode)
		assert.Len(t, backends, 1)
		assert.IsType(t, (*LocalBackend)(nil), backends[0])
	})

	t.Run("proxy-authority routes to local", func(t *testing.T) {
		md := metadata.Pairs("proxy-authority", "test", "machines", remoteTarget.Name)
		ctx := metadata.NewIncomingContext(context.Background(), md)

		mode, backends, err := d.Director(ctx, "/Test/Method")

		require.NoError(t, err)
		assert.Equal(t, proxy.One2One, mode)
		assert.Len(t, backends, 1)
		assert.IsType(t, (*LocalBackend)(nil), backends[0])
	})

	t.Run("machine singular local", func(t *testing.T) {
		d.localAddress.Store(localTarget.Addr)
		d.mapper = &mockMapper{targets: []MachineTarget{localTarget}}

		md := metadata.New(map[string]string{"machine": localTarget.Name})
		ctx := metadata.NewIncomingContext(context.Background(), md)

		mode, backends, err := d.Director(ctx, "/Test/Method")

		require.NoError(t, err)
		assert.Equal(t, proxy.One2One, mode)
		assert.Len(t, backends, 1)
		assert.IsType(t, (*LocalBackend)(nil), backends[0])
	})

	t.Run("machine singular remote", func(t *testing.T) {
		d.localAddress.Store(localTarget.Addr)
		d.mapper = &mockMapper{targets: []MachineTarget{remoteTarget}}

		md := metadata.New(map[string]string{"machine": remoteTarget.Name})
		ctx := metadata.NewIncomingContext(context.Background(), md)

		mode, backends, err := d.Director(ctx, "/Test/Method")

		require.NoError(t, err)
		assert.Equal(t, proxy.One2One, mode)
		assert.Len(t, backends, 1)
		assert.IsType(t, (*RemoteBackend)(nil), backends[0])
		assert.Equal(t, "[fd00::2]:8080", backends[0].(*RemoteBackend).target)
	})

	t.Run("machine not found", func(t *testing.T) {
		d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{"missing"}}}

		md := metadata.New(map[string]string{"machine": "missing"})
		ctx := metadata.NewIncomingContext(context.Background(), md)

		_, _, err := d.Director(ctx, "/Test/Method")

		require.Error(t, err)
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.InvalidArgument, st.Code())
		assert.Contains(t, st.Message(), "machine not found: missing")
	})

	t.Run("machines plural single local", func(t *testing.T) {
		d.localAddress.Store(localTarget.Addr)
		d.mapper = &mockMapper{targets: []MachineTarget{localTarget}}

		md := metadata.Pairs("machines", localTarget.Name)
		ctx := metadata.NewIncomingContext(context.Background(), md)

		mode, backends, err := d.Director(ctx, "/Test/Method")

		require.NoError(t, err)
		assert.Equal(t, proxy.One2Many, mode)
		assert.Len(t, backends, 1)

		mb := backends[0].(*MetadataBackend)
		assert.Equal(t, localTarget.ID, mb.MachineID)
		assert.Equal(t, localTarget.Name, mb.MachineName)
		assert.Equal(t, localTarget.Addr, mb.MachineAddr)
		assert.IsType(t, (*LocalBackend)(nil), mb.Backend)
	})

	t.Run("machines plural multiple", func(t *testing.T) {
		d.localAddress.Store(localTarget.Addr)
		d.mapper = &mockMapper{targets: []MachineTarget{localTarget, remoteTarget}}

		md := metadata.Pairs("machines", localTarget.Name, "machines", remoteTarget.Name)
		ctx := metadata.NewIncomingContext(context.Background(), md)

		mode, backends, err := d.Director(ctx, "/Test/Method")

		require.NoError(t, err)
		assert.Equal(t, proxy.One2Many, mode)
		assert.Len(t, backends, 2)

		// First backend should be local.
		mb0 := backends[0].(*MetadataBackend)
		assert.Equal(t, localTarget.ID, mb0.MachineID)
		assert.IsType(t, (*LocalBackend)(nil), mb0.Backend)

		// Second backend should be remote.
		mb1 := backends[1].(*MetadataBackend)
		assert.Equal(t, remoteTarget.ID, mb1.MachineID)
		assert.IsType(t, (*RemoteBackend)(nil), mb1.Backend)
		assert.Equal(t, "[fd00::2]:8080", mb1.Backend.(*RemoteBackend).target)
	})

	t.Run("machines empty string", func(t *testing.T) {
		d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{""}}}

		md := metadata.Pairs("machines", "")
		ctx := metadata.NewIncomingContext(context.Background(), md)

		_, _, err := d.Director(ctx, "/Test/Method")

		require.Error(t, err)
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.InvalidArgument, st.Code())
		assert.Contains(t, st.Message(), "machine not found")
	})

	t.Run("machine empty slice", func(t *testing.T) {
		md := metadata.MD{"machine": []string{}}
		ctx := metadata.NewIncomingContext(context.Background(), md)

		_, _, err := d.Director(ctx, "/Test/Method")

		require.Error(t, err)
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.InvalidArgument, st.Code())
		assert.Contains(t, st.Message(), "proxy metadata 'machine' must have exactly one value")
	})

	t.Run("machines empty slice", func(t *testing.T) {
		md := metadata.MD{"machines": []string{}}
		ctx := metadata.NewIncomingContext(context.Background(), md)

		_, _, err := d.Director(ctx, "/Test/Method")

		require.Error(t, err)
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.InvalidArgument, st.Code())
		assert.Contains(t, st.Message(), "proxy metadata 'machines' is empty")
	})

	t.Run("both machine and machines set", func(t *testing.T) {
		md := metadata.Pairs("machine", "m1", "machines", "m1", "machines", "m2")
		ctx := metadata.NewIncomingContext(context.Background(), md)

		_, _, err := d.Director(ctx, "/Test/Method")

		require.Error(t, err)
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.InvalidArgument, st.Code())
		assert.Contains(t, st.Message(), "both 'machine' and 'machines' proxy metadata are set")
	})

	t.Run("machines not found", func(t *testing.T) {
		d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{"missing"}}}

		md := metadata.Pairs("machines", "missing")
		ctx := metadata.NewIncomingContext(context.Background(), md)

		_, _, err := d.Director(ctx, "/Test/Method")

		require.Error(t, err)
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.InvalidArgument, st.Code())
	})

	t.Run("machines mapper generic error", func(t *testing.T) {
		d.mapper = &mockMapper{err: errors.New("boom")}

		md := metadata.Pairs("machines", "any")
		ctx := metadata.NewIncomingContext(context.Background(), md)

		_, _, err := d.Director(ctx, "/Test/Method")

		require.Error(t, err)
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.Internal, st.Code())
	})
}

func TestMapErrorToStatus(t *testing.T) {
	t.Run("machines not found", func(t *testing.T) {
		err := mapErrorToStatus(&MachinesNotFoundError{NotFound: []string{"a", "b"}})
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.InvalidArgument, st.Code())
	})

	t.Run("already grpc status", func(t *testing.T) {
		original := status.Error(codes.DeadlineExceeded, "timeout")
		err := mapErrorToStatus(original)
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.DeadlineExceeded, st.Code())
	})

	t.Run("generic error", func(t *testing.T) {
		err := mapErrorToStatus(errors.New("something broke"))
		st, ok := status.FromError(err)
		require.True(t, ok)
		assert.Equal(t, codes.Internal, st.Code())
		assert.Contains(t, st.Message(), "something broke")
	})
}
