package regionalservice

import (
	"context"
	"testing"

	awssdk "github.com/aws/aws-sdk-go-v2/aws"
	ec2api "github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/aws/smithy-go"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
)

type fakeByocRouteClient struct {
	routes        map[string][]ec2types.Route
	createdRoutes []createRouteCall
	deletedRoutes []deleteRouteCall
	deleteErr     error
}

type createRouteCall struct {
	routeTableID string
	cidr         string
	peeringID    string
}

type deleteRouteCall struct {
	routeTableID string
	cidr         string
}

func (f *fakeByocRouteClient) DescribeRouteTables(ctx context.Context, input *ec2api.DescribeRouteTablesInput, optFns ...func(*ec2api.Options)) (*ec2api.DescribeRouteTablesOutput, error) {
	out := &ec2api.DescribeRouteTablesOutput{}
	for _, id := range input.RouteTableIds {
		out.RouteTables = append(out.RouteTables, ec2types.RouteTable{
			RouteTableId: awssdk.String(id),
			Routes:       f.routes[id],
		})
	}
	return out, nil
}

func (f *fakeByocRouteClient) CreateRoute(ctx context.Context, input *ec2api.CreateRouteInput, optFns ...func(*ec2api.Options)) (*ec2api.CreateRouteOutput, error) {
	f.createdRoutes = append(f.createdRoutes, createRouteCall{
		routeTableID: awssdk.ToString(input.RouteTableId),
		cidr:         awssdk.ToString(input.DestinationCidrBlock),
		peeringID:    awssdk.ToString(input.VpcPeeringConnectionId),
	})
	return &ec2api.CreateRouteOutput{Return: awssdk.Bool(true)}, nil
}

func (f *fakeByocRouteClient) DeleteRoute(ctx context.Context, input *ec2api.DeleteRouteInput, optFns ...func(*ec2api.Options)) (*ec2api.DeleteRouteOutput, error) {
	f.deletedRoutes = append(f.deletedRoutes, deleteRouteCall{
		routeTableID: awssdk.ToString(input.RouteTableId),
		cidr:         awssdk.ToString(input.DestinationCidrBlock),
	})
	if f.deleteErr != nil {
		return nil, f.deleteErr
	}
	return &ec2api.DeleteRouteOutput{}, nil
}

func TestEnsureBYOCVPCPeeringRoutesCreatesMissingRoutes(t *testing.T) {
	cli := &fakeByocRouteClient{routes: map[string][]ec2types.Route{
		"rtb-a": {},
		"rtb-b": {},
	}}
	req := message.CreateClusterReq{
		BYOCVPCPeering: &message.BYOCVPCPeering{
			PeeringConnectionID: "pcx-123",
			DataplaneVPCCIDR:    "10.30.0.0/16",
		},
	}
	cfg := structs.BYOCVPCPeeringConfig{RouteTableIDs: []string{"rtb-a", "rtb-b"}}

	err := ensureBYOCVPCPeeringRoutes(context.Background(), req, cfg, cli)

	require.NoError(t, err)
	assert.Equal(t, []createRouteCall{
		{routeTableID: "rtb-a", cidr: "10.30.0.0/16", peeringID: "pcx-123"},
		{routeTableID: "rtb-b", cidr: "10.30.0.0/16", peeringID: "pcx-123"},
	}, cli.createdRoutes)
}

func TestEnsureBYOCVPCPeeringRoutesSkipsExistingRoutes(t *testing.T) {
	cli := &fakeByocRouteClient{routes: map[string][]ec2types.Route{
		"rtb-a": {{DestinationCidrBlock: awssdk.String("10.30.0.0/16"), VpcPeeringConnectionId: awssdk.String("pcx-123")}},
	}}
	req := message.CreateClusterReq{
		BYOCVPCPeering: &message.BYOCVPCPeering{
			PeeringConnectionID: "pcx-123",
			DataplaneVPCCIDR:    "10.30.0.0/16",
		},
	}
	cfg := structs.BYOCVPCPeeringConfig{RouteTableIDs: []string{"rtb-a"}}

	err := ensureBYOCVPCPeeringRoutes(context.Background(), req, cfg, cli)

	require.NoError(t, err)
	assert.Empty(t, cli.createdRoutes)
}

func TestEnsureBYOCVPCPeeringRoutesRejectsConflictingRoute(t *testing.T) {
	cli := &fakeByocRouteClient{routes: map[string][]ec2types.Route{
		"rtb-a": {{DestinationCidrBlock: awssdk.String("10.30.0.0/16"), VpcPeeringConnectionId: awssdk.String("pcx-old")}},
	}}
	req := message.CreateClusterReq{
		BYOCVPCPeering: &message.BYOCVPCPeering{
			PeeringConnectionID: "pcx-123",
			DataplaneVPCCIDR:    "10.30.0.0/16",
		},
	}
	cfg := structs.BYOCVPCPeeringConfig{RouteTableIDs: []string{"rtb-a"}}

	err := ensureBYOCVPCPeeringRoutes(context.Background(), req, cfg, cli)

	require.Error(t, err)
	assert.Contains(t, err.Error(), "already targets pcx-old")
	assert.Empty(t, cli.createdRoutes)
}

func TestDeleteBYOCVPCPeeringRoutesDeletesMatchingRoutes(t *testing.T) {
	cli := &fakeByocRouteClient{routes: map[string][]ec2types.Route{
		"rtb-a": {{DestinationCidrBlock: awssdk.String("10.30.0.0/16"), VpcPeeringConnectionId: awssdk.String("pcx-123")}},
		"rtb-b": {{DestinationCidrBlock: awssdk.String("10.30.0.0/16"), VpcPeeringConnectionId: awssdk.String("pcx-123")}},
	}}
	cfg := structs.BYOCVPCPeeringConfig{RouteTableIDs: []string{"rtb-a", "rtb-b"}}

	err := deleteBYOCVPCPeeringRoutes(context.Background(), "pcx-123", "10.30.0.0/16", cfg, cli)

	require.NoError(t, err)
	assert.Equal(t, []deleteRouteCall{
		{routeTableID: "rtb-a", cidr: "10.30.0.0/16"},
		{routeTableID: "rtb-b", cidr: "10.30.0.0/16"},
	}, cli.deletedRoutes)
}

func TestDeleteBYOCVPCPeeringRoutesSkipsAbsentRoutes(t *testing.T) {
	cli := &fakeByocRouteClient{routes: map[string][]ec2types.Route{"rtb-a": {}}}
	cfg := structs.BYOCVPCPeeringConfig{RouteTableIDs: []string{"rtb-a"}}

	err := deleteBYOCVPCPeeringRoutes(context.Background(), "pcx-123", "10.30.0.0/16", cfg, cli)

	require.NoError(t, err)
	assert.Empty(t, cli.deletedRoutes)
}

func TestDeleteBYOCVPCPeeringRoutesRejectsConflictingRoute(t *testing.T) {
	cli := &fakeByocRouteClient{routes: map[string][]ec2types.Route{
		"rtb-a": {{DestinationCidrBlock: awssdk.String("10.30.0.0/16"), VpcPeeringConnectionId: awssdk.String("pcx-old")}},
	}}
	cfg := structs.BYOCVPCPeeringConfig{RouteTableIDs: []string{"rtb-a"}}

	err := deleteBYOCVPCPeeringRoutes(context.Background(), "pcx-123", "10.30.0.0/16", cfg, cli)

	require.Error(t, err)
	assert.Contains(t, err.Error(), "targets unexpected pcx-old")
	assert.Empty(t, cli.deletedRoutes)
}

func TestDeleteBYOCVPCPeeringRoutesIgnoresRouteNotFound(t *testing.T) {
	cli := &fakeByocRouteClient{
		routes: map[string][]ec2types.Route{
			"rtb-a": {{DestinationCidrBlock: awssdk.String("10.30.0.0/16"), VpcPeeringConnectionId: awssdk.String("pcx-123")}},
		},
		deleteErr: &smithy.GenericAPIError{Code: "InvalidRoute.NotFound", Message: "route not found"},
	}
	cfg := structs.BYOCVPCPeeringConfig{RouteTableIDs: []string{"rtb-a"}}

	err := deleteBYOCVPCPeeringRoutes(context.Background(), "pcx-123", "10.30.0.0/16", cfg, cli)

	require.NoError(t, err)
	assert.Equal(t, []deleteRouteCall{{routeTableID: "rtb-a", cidr: "10.30.0.0/16"}}, cli.deletedRoutes)
}
