package router

import (
	"context"
	"errors"
	"log/slog"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/fieldz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/nettool"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/matcher"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_modshared"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_tunnel_tunserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testhelpers"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tunnel/tunserver"
	metricnoop "go.opentelemetry.io/otel/metric/noop"
	"go.opentelemetry.io/otel/trace/noop"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc/metadata"
	clock_testing "k8s.io/utils/clock/testing"
)

func TestPlugin_FindReadyGateway_Timeout_ConnectedRecently(t *testing.T) {
	ctrl := gomock.NewController(t)
	af := NewMockAgentFinder(ctrl)
	agentLastConnected := time.Unix(1, 0)
	modAPI := mock_modshared.NewMockAPI(ctrl)
	log := testlogger.New(t)
	tf := NewMockTunnelFinder(ctrl)
	gf := mock_tunnel_tunserver.NewMockGatewayFinder[nettool.MultiURL](ctrl)

	gomock.InOrder(
		gf.EXPECT().
			Find(gomock.Any()).
			DoAndReturn(func(ctx context.Context) (tunserver.ReadyGateway[nettool.MultiURL], error) {
				<-ctx.Done()
				return tunserver.ReadyGateway[nettool.MultiURL]{}, ctx.Err()
			}),
		af.EXPECT().
			AgentLastConnected(gomock.Any(), testhelpers.AgentkKey1).
			Return(agentLastConnected, nil),
		modAPI.EXPECT().
			HandleProcessingError(gomock.Any(), gomock.Any(), "Finding tunnel timed out",
				matcher.ErrorEq("agent was recently connected"), fieldz.AgentKey(testhelpers.AgentkKey1)),
	)
	clk := clock_testing.NewFakePassiveClock(time.Unix(2, 0))
	tr := noop.NewTracerProvider().Tracer("test")
	m := metricnoop.NewMeterProvider().Meter("test")
	p, err := newPlugin(modAPI, af, tf, tr, m, time.Millisecond, clk,
		func(outgoingCtx context.Context, log *slog.Logger, fullMethod string, agentKey api.AgentKey) tunserver.GatewayFinder[nettool.MultiURL] {
			return gf
		})
	require.NoError(t, err)

	md := SetRoutingMetadata(nil, testhelpers.AgentkKey1)
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	_, _, _, err = p.FindReadyGateway(ctx, log, "/a/b")
	assert.EqualError(t, err, "rpc error: code = DeadlineExceeded desc = finding tunnel timed out. Agent last connected 1 second(s) ago")
}

func TestPlugin_FindReadyGateway_Timeout_NotConnectedRecently(t *testing.T) {
	ctrl := gomock.NewController(t)
	af := NewMockAgentFinder(ctrl)
	modAPI := mock_modshared.NewMockAPI(ctrl)
	log := testlogger.New(t)
	tf := NewMockTunnelFinder(ctrl)
	gf := mock_tunnel_tunserver.NewMockGatewayFinder[nettool.MultiURL](ctrl)

	gomock.InOrder(
		gf.EXPECT().
			Find(gomock.Any()).
			DoAndReturn(func(ctx context.Context) (tunserver.ReadyGateway[nettool.MultiURL], error) {
				<-ctx.Done()
				return tunserver.ReadyGateway[nettool.MultiURL]{}, ctx.Err()
			}),
		af.EXPECT().
			AgentLastConnected(gomock.Any(), testhelpers.AgentkKey1).
			Return(time.Time{}, nil),
	)
	clk := clock_testing.NewFakePassiveClock(time.Unix(2, 0))
	tr := noop.NewTracerProvider().Tracer("test")
	m := metricnoop.NewMeterProvider().Meter("test")
	p, err := newPlugin(modAPI, af, tf, tr, m, time.Millisecond, clk,
		func(outgoingCtx context.Context, log *slog.Logger, fullMethod string, agentKey api.AgentKey) tunserver.GatewayFinder[nettool.MultiURL] {
			return gf
		})
	require.NoError(t, err)

	md := SetRoutingMetadata(nil, testhelpers.AgentkKey1)
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	_, _, _, err = p.FindReadyGateway(ctx, log, "/a/b")
	assert.EqualError(t, err, "rpc error: code = DeadlineExceeded desc = finding tunnel timed out. Agent hasn't connected recently. Make sure the agent is connected and up to date")
}

func TestPlugin_FindReadyGateway_Timeout_LastConnectedError(t *testing.T) {
	ctrl := gomock.NewController(t)
	af := NewMockAgentFinder(ctrl)
	modAPI := mock_modshared.NewMockAPI(ctrl)
	log := testlogger.New(t)
	tf := NewMockTunnelFinder(ctrl)
	gf := mock_tunnel_tunserver.NewMockGatewayFinder[nettool.MultiURL](ctrl)

	gomock.InOrder(
		gf.EXPECT().
			Find(gomock.Any()).
			DoAndReturn(func(ctx context.Context) (tunserver.ReadyGateway[nettool.MultiURL], error) {
				<-ctx.Done()
				return tunserver.ReadyGateway[nettool.MultiURL]{}, ctx.Err()
			}),
		af.EXPECT().
			AgentLastConnected(gomock.Any(), testhelpers.AgentkKey1).
			Return(time.Time{}, errors.New("boom")),
		modAPI.EXPECT().
			HandleProcessingError(gomock.Any(), gomock.Any(), "Unable to correlate tunnel timeout error with agent registration data",
				matcher.ErrorEq("boom"), fieldz.AgentKey(testhelpers.AgentkKey1)),
	)
	clk := clock_testing.NewFakePassiveClock(time.Unix(2, 0))
	tr := noop.NewTracerProvider().Tracer("test")
	m := metricnoop.NewMeterProvider().Meter("test")
	p, err := newPlugin(modAPI, af, tf, tr, m, time.Millisecond, clk,
		func(outgoingCtx context.Context, log *slog.Logger, fullMethod string, agentKey api.AgentKey) tunserver.GatewayFinder[nettool.MultiURL] {
			return gf
		})
	require.NoError(t, err)

	md := SetRoutingMetadata(nil, testhelpers.AgentkKey1)
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	_, _, _, err = p.FindReadyGateway(ctx, log, "/a/b")
	assert.EqualError(t, err, "rpc error: code = DeadlineExceeded desc = finding tunnel timed out. Agent hasn't connected recently. Make sure the agent is connected and up to date")
}
