package console

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
	"github.com/tidbcloud/dataflow-service/app/models/domain"
	private_link_mock "github.com/tidbcloud/dataflow-service/app/models/private_link/mock"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	"github.com/tidbcloud/dataflow-service/idl/pbgen/proto/dataflow"
	"github.com/tidbcloud/pkgv2/bizerr"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type sinkEndpointControllerTestSuite struct {
	suite.Suite
	ctx                   context.Context
	controller            *sinkEndpointControllerImpl
	mockSinkEndpointModel *private_link_mock.SinkEndpointModel
}

func TestSinkEndpointControllerTestSuite(t *testing.T) {
	suite.Run(t, new(sinkEndpointControllerTestSuite))
}

func (s *sinkEndpointControllerTestSuite) SetupTest() {
	s.ctx = context.Background()
	s.mockSinkEndpointModel = private_link_mock.NewSinkEndpointModel(s.T())
	s.controller = NewSinkEndpointController(s.mockSinkEndpointModel).(*sinkEndpointControllerImpl)
}

func (s *sinkEndpointControllerTestSuite) TestListSinkEndpoints_Success() {
	req := &dataflow.ListSinkEndpointsRequest{
		OrgId:     1,
		ProjectId: 2,
		Provider:  "aws",
		Region:    "us-east-1",
	}

	now := time.Now()
	endpoint1 := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: now,
			},
		},
		TenantID:      entity.ID(1),
		ProjectID:     entity.ID(2),
		CloudProvider: entity.CloudProviderTypeAWS,
		Region:        "us-east-1",
		Name:          "endpoint-1",
		ServiceName:   "service-1",
		Status:        entity.BizPLEndpointStatusAvailable,
		DeletedTime:   0,
	}

	unusedAt := now.Add(-5 * 24 * time.Hour) // 5 days ago
	endpoint2 := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(2),
				CreatedAt: now,
			},
		},
		TenantID:      entity.ID(1),
		ProjectID:     entity.ID(2),
		CloudProvider: entity.CloudProviderTypeAWS,
		Region:        "us-east-1",
		Name:          "endpoint-2",
		ServiceName:   "service-2",
		Status:        entity.BizPLEndpointStatusAvailable,
		UnusedAt:      &unusedAt, // Will delete at UnusedAt + 30 days
		DeletedTime:   0,
	}

	usageInfo1 := &domain.SinkEndpointUsageInfo{
		ChangefeedIds: []uint64{10, 20},
		LastUsedAt:    &now,
	}

	usageInfo2 := &domain.SinkEndpointUsageInfo{
		ChangefeedIds: []uint64{30},
		LastUsedAt:    nil,
	}

	modelResp := &domain.ListSinkEndpointsResp{
		SinkEndpoints: []domain.SinkEndpointWithUsage{
			{
				Endpoint:  &endpoint1,
				UsageInfo: usageInfo1,
			},
			{
				Endpoint:  &endpoint2,
				UsageInfo: usageInfo2,
			},
		},
	}

	s.mockSinkEndpointModel.On("ListSinkEndpoint", s.ctx, mock.AnythingOfType("*domain.ListSinkEndpointsReq")).
		Return(modelResp, nil)

	resp, err := s.controller.ListSinkEndpoints(s.ctx, req)
	s.NoError(err)
	s.NotNil(resp)
	s.Equal(2, len(resp.SinkEndpoints))

	// Verify first endpoint
	s.Equal(uint64(1), resp.SinkEndpoints[0].Id)
	s.Equal("endpoint-1", resp.SinkEndpoints[0].EndpointName)
	s.NotNil(resp.SinkEndpoints[0].UsedBy)
	s.Equal([]uint64{10, 20}, resp.SinkEndpoints[0].UsedBy.ChangefeedIds)
	s.NotNil(resp.SinkEndpoints[0].LastUsedAt)
	s.Nil(resp.SinkEndpoints[0].WillDeleteAt)

	// Verify second endpoint
	s.Equal(uint64(2), resp.SinkEndpoints[1].Id)
	s.Equal("endpoint-2", resp.SinkEndpoints[1].EndpointName)
	s.NotNil(resp.SinkEndpoints[1].UsedBy)
	s.Equal([]uint64{30}, resp.SinkEndpoints[1].UsedBy.ChangefeedIds)
	s.Nil(resp.SinkEndpoints[1].LastUsedAt)
	s.NotNil(resp.SinkEndpoints[1].WillDeleteAt)
	// Will delete at UnusedAt + 30 days
	expectedWillDeleteAt := unusedAt.AddDate(0, 0, 30)
	s.Equal(expectedWillDeleteAt.Unix(), resp.SinkEndpoints[1].WillDeleteAt.Seconds)

	s.mockSinkEndpointModel.AssertExpectations(s.T())
}

func (s *sinkEndpointControllerTestSuite) TestListSinkEndpoints_Empty() {
	req := &dataflow.ListSinkEndpointsRequest{
		OrgId:     1,
		ProjectId: 2,
	}

	modelResp := &domain.ListSinkEndpointsResp{
		SinkEndpoints: []domain.SinkEndpointWithUsage{},
	}

	s.mockSinkEndpointModel.On("ListSinkEndpoint", s.ctx, mock.AnythingOfType("*domain.ListSinkEndpointsReq")).
		Return(modelResp, nil)

	resp, err := s.controller.ListSinkEndpoints(s.ctx, req)
	s.NoError(err)
	s.NotNil(resp)
	s.Equal(0, len(resp.SinkEndpoints))
	s.mockSinkEndpointModel.AssertExpectations(s.T())
}

func (s *sinkEndpointControllerTestSuite) TestListSinkEndpoints_Error() {
	req := &dataflow.ListSinkEndpointsRequest{
		OrgId:     1,
		ProjectId: 2,
	}

	mockErr := bizerr.New(bizerr.ErrCodeDB, "database error")
	s.mockSinkEndpointModel.On("ListSinkEndpoint", s.ctx, mock.AnythingOfType("*domain.ListSinkEndpointsReq")).
		Return(nil, mockErr)

	resp, err := s.controller.ListSinkEndpoints(s.ctx, req)
	s.Error(err)
	s.Nil(resp)
	s.Equal(mockErr, err)
	s.mockSinkEndpointModel.AssertExpectations(s.T())
}

func (s *sinkEndpointControllerTestSuite) TestGetSinkEndpoint_Success() {
	req := &dataflow.GetSinkEndpointRequest{
		OrgId:          1,
		ProjectId:      2,
		SinkEndpointId: 100,
	}

	now := time.Now()
	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(100),
				CreatedAt: now,
			},
		},
		TenantID:      entity.ID(1),
		ProjectID:     entity.ID(2),
		CloudProvider: entity.CloudProviderTypeAWS,
		Region:        "us-east-1",
		Name:          "test-endpoint",
		ServiceName:   "test-service",
		Status:        entity.BizPLEndpointStatusAvailable,
		DeletedTime:   0,
	}

	usageInfo := &domain.SinkEndpointUsageInfo{
		ChangefeedIds: []uint64{50, 60},
		LastUsedAt:    &now,
	}

	modelResp := &domain.QuerySinkEndpointResp{
		SinkEndpoint: &endpoint,
		UsageInfo:    usageInfo,
	}

	s.mockSinkEndpointModel.On("QuerySinkEndpoint", s.ctx, mock.AnythingOfType("*domain.QuerySinkEndpointReq")).
		Return(modelResp, nil)

	resp, err := s.controller.GetSinkEndpoint(s.ctx, req)
	s.NoError(err)
	s.NotNil(resp)
	s.Equal(uint64(100), resp.Id)
	s.Equal("test-endpoint", resp.EndpointName)
	s.NotNil(resp.UsedBy)
	s.Equal([]uint64{50, 60}, resp.UsedBy.ChangefeedIds)
	s.NotNil(resp.LastUsedAt)
	s.Nil(resp.WillDeleteAt)

	s.mockSinkEndpointModel.AssertExpectations(s.T())
}

func (s *sinkEndpointControllerTestSuite) TestGetSinkEndpoint_PermissionDenied() {
	req := &dataflow.GetSinkEndpointRequest{
		OrgId:          1,
		ProjectId:      2,
		SinkEndpointId: 100,
	}

	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID: entity.ID(100),
			},
		},
		TenantID:  entity.ID(999), // Different tenant
		ProjectID: entity.ID(2),
	}

	modelResp := &domain.QuerySinkEndpointResp{
		SinkEndpoint: &endpoint,
		UsageInfo:    nil,
	}

	s.mockSinkEndpointModel.On("QuerySinkEndpoint", s.ctx, mock.AnythingOfType("*domain.QuerySinkEndpointReq")).
		Return(modelResp, nil)

	resp, err := s.controller.GetSinkEndpoint(s.ctx, req)
	s.Error(err)
	s.Nil(resp)
	s.mockSinkEndpointModel.AssertExpectations(s.T())
}

func (s *sinkEndpointControllerTestSuite) TestGetSinkEndpoint_WithWillDeleteAt_FromUnusedAt() {
	req := &dataflow.GetSinkEndpointRequest{
		OrgId:          1,
		ProjectId:      2,
		SinkEndpointId: 200,
	}

	unusedAt := time.Now().Add(-10 * 24 * time.Hour) // 10 days ago
	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID: entity.ID(200),
			},
		},
		TenantID:    entity.ID(1),
		ProjectID:   entity.ID(2),
		UnusedAt:    &unusedAt, // Will delete at UnusedAt + 30 days
		DeletedTime: 0,
	}

	modelResp := &domain.QuerySinkEndpointResp{
		SinkEndpoint: &endpoint,
		UsageInfo:    nil,
	}

	s.mockSinkEndpointModel.On("QuerySinkEndpoint", s.ctx, mock.AnythingOfType("*domain.QuerySinkEndpointReq")).
		Return(modelResp, nil)

	resp, err := s.controller.GetSinkEndpoint(s.ctx, req)
	s.NoError(err)
	s.NotNil(resp)
	s.NotNil(resp.WillDeleteAt)
	expectedWillDeleteAt := unusedAt.AddDate(0, 0, 30)
	s.Equal(expectedWillDeleteAt.Unix(), resp.WillDeleteAt.Seconds)

	s.mockSinkEndpointModel.AssertExpectations(s.T())
}

func (s *sinkEndpointControllerTestSuite) TestGetSinkEndpoint_WithWillDeleteAt_FromDeletedTime() {
	req := &dataflow.GetSinkEndpointRequest{
		OrgId:          1,
		ProjectId:      2,
		SinkEndpointId: 201,
	}

	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID: entity.ID(201),
			},
		},
		TenantID:    entity.ID(1),
		ProjectID:   entity.ID(2),
		UnusedAt:    nil,        // No UnusedAt, use DeletedTime
		DeletedTime: 1234567890, // Unix timestamp
	}

	modelResp := &domain.QuerySinkEndpointResp{
		SinkEndpoint: &endpoint,
		UsageInfo:    nil,
	}

	s.mockSinkEndpointModel.On("QuerySinkEndpoint", s.ctx, mock.AnythingOfType("*domain.QuerySinkEndpointReq")).
		Return(modelResp, nil)

	resp, err := s.controller.GetSinkEndpoint(s.ctx, req)
	s.NoError(err)
	s.NotNil(resp)
	s.NotNil(resp.WillDeleteAt)
	s.Equal(int64(1234567890), resp.WillDeleteAt.Seconds)

	s.mockSinkEndpointModel.AssertExpectations(s.T())
}

func (s *sinkEndpointControllerTestSuite) TestCreateSinkEndpoint_Success() {
	req := &dataflow.CreateSinkEndpointRequest{
		OrgId:     1,
		ProjectId: 2,
		SinkEndpoint: &dataflow.SinkEndpoint{
			EndpointName:        "new-endpoint",
			EndpointServiceName: "new-service",
			Provider:            "aws",
			Region:              "us-east-1",
		},
	}

	now := time.Now()
	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(300),
				CreatedAt: now,
			},
		},
		TenantID:      entity.ID(1),
		ProjectID:     entity.ID(2),
		CloudProvider: entity.CloudProviderTypeAWS,
		Region:        "us-east-1",
		Name:          "new-endpoint",
		ServiceName:   "new-service",
		Status:        entity.BizPLEndpointStatusCreating,
	}

	modelResp := &domain.CreateSinkEndpointResp{
		SinkEndpoint: &endpoint,
	}

	s.mockSinkEndpointModel.On("CreateSinkEndpoint", s.ctx, mock.AnythingOfType("*domain.CreateSinkEndpointReq")).
		Return(modelResp, nil)

	resp, err := s.controller.CreateSinkEndpoint(s.ctx, req)
	s.NoError(err)
	s.NotNil(resp)
	s.Equal(uint64(300), resp.Id)
	s.Equal("new-endpoint", resp.EndpointName)
	// Newly created endpoint has no usage info
	s.Nil(resp.UsedBy)
	s.Nil(resp.LastUsedAt)

	s.mockSinkEndpointModel.AssertExpectations(s.T())
}

func (s *sinkEndpointControllerTestSuite) TestSinkEndpointWithUsageInfo2Dataflow_NoUsageInfo() {
	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: time.Now(),
			},
		},
		Name:        "test",
		ServiceName: "service",
		Status:      entity.BizPLEndpointStatusAvailable,
		DeletedTime: 0,
	}

	result := sinkEndpointWithUsageInfo2Dataflow(&endpoint, nil)
	s.NotNil(result)
	s.Equal(uint64(1), result.Id)
	s.Nil(result.UsedBy)
	s.Nil(result.LastUsedAt)
	s.Nil(result.WillDeleteAt)
}

func (s *sinkEndpointControllerTestSuite) TestSinkEndpointWithUsageInfo2Dataflow_WithUsageInfo() {
	now := time.Now()
	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: now,
			},
		},
		Name:        "test",
		ServiceName: "service",
		Status:      entity.BizPLEndpointStatusAvailable,
		DeletedTime: 0,
	}

	usageInfo := &domain.SinkEndpointUsageInfo{
		ChangefeedIds: []uint64{1, 2, 3},
		LastUsedAt:    &now,
	}

	result := sinkEndpointWithUsageInfo2Dataflow(&endpoint, usageInfo)
	s.NotNil(result)
	s.NotNil(result.UsedBy)
	s.Equal([]uint64{1, 2, 3}, result.UsedBy.ChangefeedIds)
	s.NotNil(result.LastUsedAt)
	s.Equal(timestamppb.New(now).Seconds, result.LastUsedAt.Seconds)
}

func (s *sinkEndpointControllerTestSuite) TestSinkEndpointWithUsageInfo2Dataflow_WithUnusedAt() {
	unusedAt := time.Now().Add(-5 * 24 * time.Hour) // 5 days ago
	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: time.Now(),
			},
		},
		Name:        "test",
		ServiceName: "service",
		Status:      entity.BizPLEndpointStatusAvailable,
		UnusedAt:    &unusedAt,
		DeletedTime: 0,
	}

	result := sinkEndpointWithUsageInfo2Dataflow(&endpoint, nil)
	s.NotNil(result)
	s.NotNil(result.WillDeleteAt)
	expectedWillDeleteAt := unusedAt.AddDate(0, 0, 30)
	s.Equal(expectedWillDeleteAt.Unix(), result.WillDeleteAt.Seconds)
}

func (s *sinkEndpointControllerTestSuite) TestSinkEndpointWithUsageInfo2Dataflow_WithDeletedTime() {
	endpoint := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: time.Now(),
			},
		},
		Name:        "test",
		ServiceName: "service",
		Status:      entity.BizPLEndpointStatusAvailable,
		UnusedAt:    nil, // No UnusedAt, use DeletedTime
		DeletedTime: 1234567890,
	}

	result := sinkEndpointWithUsageInfo2Dataflow(&endpoint, nil)
	s.NotNil(result)
	s.NotNil(result.WillDeleteAt)
	s.Equal(int64(1234567890), result.WillDeleteAt.Seconds)
}
