package impl

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"github.com/tidbcloud/dataflow-service/app/models/domain"
	"github.com/tidbcloud/dataflow-service/app/repo"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	repo_mock "github.com/tidbcloud/dataflow-service/app/repo/mock"
	regionalapi_mock "github.com/tidbcloud/dataflow-service/pkg/regional-api/mock"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"
)

type sinkEndpointImplTestSuite struct {
	suite.Suite
	ctx                        context.Context
	model                      *sinkEndpointImpl
	db                         *gorm.DB
	transProvider              db.TransProvider
	mockCli                    *regionalapi_mock.TiCDCPrivateLinkClient
	mockBizPLEndpointRepo      *repo_mock.BizPrivateLinkEndpointRepo
	mockBizPLEndpointUsageRepo *repo_mock.BizPrivateLinkEndpointUsageRepo
}

func TestSinkEndpointImplTestSuite(t *testing.T) {
	suite.Run(t, new(sinkEndpointImplTestSuite))
}

func (s *sinkEndpointImplTestSuite) SetupTest() {
	s.ctx = context.Background()
	testDB, err := repo.GetFakeDB()
	require.NoError(s.T(), err)
	s.db = testDB
	s.transProvider = db.NewProvider(s.db)
	s.mockCli = regionalapi_mock.NewTiCDCPrivateLinkClient(s.T())
	s.mockBizPLEndpointRepo = repo_mock.NewBizPrivateLinkEndpointRepo(s.T())
	s.mockBizPLEndpointUsageRepo = repo_mock.NewBizPrivateLinkEndpointUsageRepo(s.T())

	s.model = NewSinkEndpointImpl(
		s.transProvider,
		s.mockCli,
		s.mockBizPLEndpointRepo,
		s.mockBizPLEndpointUsageRepo,
	).(*sinkEndpointImpl)
}

func (s *sinkEndpointImplTestSuite) TestGetSinkEndpointUsageInfo_WithUsages() {
	now := time.Now()
	endpoint := &entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID: entity.ID(1),
			},
		},
		UnusedAt: nil,
	}

	usages := []entity.BizPrivateLinkEndpointUsage{
		{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: now.Add(-2 * time.Hour),
			},
			BizPrivateLinkEndpointID: entity.ID(1),
			BusinessType:             entity.BizPLEndpointUsageBusinessTypeChangefeed,
			BusinessID:               entity.ID(10),
		},
		{
			Base: entity.Base{
				ID:        entity.ID(2),
				CreatedAt: now.Add(-1 * time.Hour),
			},
			BizPrivateLinkEndpointID: entity.ID(1),
			BusinessType:             entity.BizPLEndpointUsageBusinessTypeChangefeed,
			BusinessID:               entity.ID(20),
		},
	}

	s.mockBizPLEndpointUsageRepo.On("List", s.ctx, mock.AnythingOfType("repo.BizPrivateLinkEndpointUsageFilterOP")).
		Return(usages, nil)

	usageInfo := s.model.getSinkEndpointUsageInfo(s.ctx, endpoint)

	s.NotNil(usageInfo)
	s.Equal(2, len(usageInfo.ChangefeedIds))
	s.Contains(usageInfo.ChangefeedIds, uint64(10))
	s.Contains(usageInfo.ChangefeedIds, uint64(20))
	// Should use the latest CreatedAt
	s.NotNil(usageInfo.LastUsedAt)
	s.True(usageInfo.LastUsedAt.After(now.Add(-2 * time.Hour)))
	s.True(usageInfo.LastUsedAt.Before(now.Add(-1*time.Hour)) || usageInfo.LastUsedAt.Equal(now.Add(-1*time.Hour)))

	s.mockBizPLEndpointUsageRepo.AssertExpectations(s.T())
}

func (s *sinkEndpointImplTestSuite) TestGetSinkEndpointUsageInfo_WithUnusedAt() {
	now := time.Now()
	unusedAt := now.Add(-5 * time.Hour)
	endpoint := &entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID: entity.ID(1),
			},
		},
		UnusedAt: &unusedAt,
	}

	usages := []entity.BizPrivateLinkEndpointUsage{
		{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: now.Add(-2 * time.Hour),
			},
			BizPrivateLinkEndpointID: entity.ID(1),
			BusinessType:             entity.BizPLEndpointUsageBusinessTypeChangefeed,
			BusinessID:               entity.ID(10),
		},
	}

	s.mockBizPLEndpointUsageRepo.On("List", s.ctx, mock.AnythingOfType("repo.BizPrivateLinkEndpointUsageFilterOP")).
		Return(usages, nil)

	usageInfo := s.model.getSinkEndpointUsageInfo(s.ctx, endpoint)

	s.NotNil(usageInfo)
	s.Equal(1, len(usageInfo.ChangefeedIds))
	// Should prefer UnusedAt over CreatedAt
	s.NotNil(usageInfo.LastUsedAt)
	s.Equal(unusedAt, *usageInfo.LastUsedAt)

	s.mockBizPLEndpointUsageRepo.AssertExpectations(s.T())
}

func (s *sinkEndpointImplTestSuite) TestGetSinkEndpointUsageInfo_NoUsages() {
	endpoint := &entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID: entity.ID(1),
			},
		},
		UnusedAt: nil,
	}

	s.mockBizPLEndpointUsageRepo.On("List", s.ctx, mock.AnythingOfType("repo.BizPrivateLinkEndpointUsageFilterOP")).
		Return([]entity.BizPrivateLinkEndpointUsage{}, nil)

	usageInfo := s.model.getSinkEndpointUsageInfo(s.ctx, endpoint)

	s.NotNil(usageInfo)
	s.Equal(0, len(usageInfo.ChangefeedIds))
	s.Nil(usageInfo.LastUsedAt)

	s.mockBizPLEndpointUsageRepo.AssertExpectations(s.T())
}

func (s *sinkEndpointImplTestSuite) TestGetSinkEndpointUsageInfo_NoUsagesWithUnusedAt() {
	now := time.Now()
	unusedAt := now.Add(-3 * time.Hour)
	endpoint := &entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID: entity.ID(1),
			},
		},
		UnusedAt: &unusedAt,
	}

	s.mockBizPLEndpointUsageRepo.On("List", s.ctx, mock.AnythingOfType("repo.BizPrivateLinkEndpointUsageFilterOP")).
		Return([]entity.BizPrivateLinkEndpointUsage{}, nil)

	usageInfo := s.model.getSinkEndpointUsageInfo(s.ctx, endpoint)

	s.NotNil(usageInfo)
	s.Equal(0, len(usageInfo.ChangefeedIds))
	s.NotNil(usageInfo.LastUsedAt)
	s.Equal(unusedAt, *usageInfo.LastUsedAt)

	s.mockBizPLEndpointUsageRepo.AssertExpectations(s.T())
}

func (s *sinkEndpointImplTestSuite) TestGetSinkEndpointUsageInfo_Error() {
	endpoint := &entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID: entity.ID(1),
			},
		},
		UnusedAt: nil,
	}

	mockErr := bizerr.New(bizerr.ErrCodeDB, "database error")
	s.mockBizPLEndpointUsageRepo.On("List", s.ctx, mock.AnythingOfType("repo.BizPrivateLinkEndpointUsageFilterOP")).
		Return(nil, mockErr)

	usageInfo := s.model.getSinkEndpointUsageInfo(s.ctx, endpoint)

	// Should return empty usage info on error
	s.NotNil(usageInfo)
	s.Equal(0, len(usageInfo.ChangefeedIds))
	s.Nil(usageInfo.LastUsedAt)

	s.mockBizPLEndpointUsageRepo.AssertExpectations(s.T())
}

func (s *sinkEndpointImplTestSuite) TestQuerySinkEndpoint_WithUsageInfo() {
	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),
		Name:      "test-endpoint",
		Status:    entity.BizPLEndpointStatusAvailable,
	}

	usages := []entity.BizPrivateLinkEndpointUsage{
		{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: now.Add(-1 * time.Hour),
			},
			BizPrivateLinkEndpointID: entity.ID(100),
			BusinessType:             entity.BizPLEndpointUsageBusinessTypeChangefeed,
			BusinessID:               entity.ID(50),
		},
	}

	s.mockBizPLEndpointRepo.On("Load", s.ctx, uint64(100)).
		Return(endpoint, nil)
	s.mockBizPLEndpointUsageRepo.On("List", s.ctx, mock.AnythingOfType("repo.BizPrivateLinkEndpointUsageFilterOP")).
		Return(usages, nil)

	req := &domain.QuerySinkEndpointReq{
		ID: 100,
	}

	resp, err := s.model.QuerySinkEndpoint(s.ctx, req)

	s.NoError(err)
	s.NotNil(resp)
	s.NotNil(resp.SinkEndpoint)
	s.Equal(entity.ID(100), resp.SinkEndpoint.ID)
	s.NotNil(resp.UsageInfo)
	s.Equal(1, len(resp.UsageInfo.ChangefeedIds))
	s.Equal(uint64(50), resp.UsageInfo.ChangefeedIds[0])

	s.mockBizPLEndpointRepo.AssertExpectations(s.T())
	s.mockBizPLEndpointUsageRepo.AssertExpectations(s.T())
}

func (s *sinkEndpointImplTestSuite) TestListSinkEndpoint_WithUsageInfo() {
	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),
		Name:      "endpoint-1",
		Status:    entity.BizPLEndpointStatusAvailable,
	}

	endpoint2 := entity.BizPrivateLinkEndpoint{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(2),
				CreatedAt: now,
			},
		},
		TenantID:  entity.ID(1),
		ProjectID: entity.ID(2),
		Name:      "endpoint-2",
		Status:    entity.BizPLEndpointStatusAvailable,
	}

	usages1 := []entity.BizPrivateLinkEndpointUsage{
		{
			Base: entity.Base{
				ID:        entity.ID(1),
				CreatedAt: now.Add(-1 * time.Hour),
			},
			BizPrivateLinkEndpointID: entity.ID(1),
			BusinessType:             entity.BizPLEndpointUsageBusinessTypeChangefeed,
			BusinessID:               entity.ID(10),
		},
	}

	usages2 := []entity.BizPrivateLinkEndpointUsage{}

	s.mockBizPLEndpointRepo.On("List", s.ctx, mock.AnythingOfType("repo.BizPrivateLinkEndpointFilterOP")).
		Return([]entity.BizPrivateLinkEndpoint{endpoint1, endpoint2}, nil)
	s.mockBizPLEndpointUsageRepo.On("List", s.ctx, mock.MatchedBy(func(filter repo.BizPrivateLinkEndpointUsageFilterOP) bool {
		// This is a simplified matcher - in real test you might want to verify the filter more precisely
		return true
	})).Return(usages1, nil).Once()
	s.mockBizPLEndpointUsageRepo.On("List", s.ctx, mock.MatchedBy(func(filter repo.BizPrivateLinkEndpointUsageFilterOP) bool {
		return true
	})).Return(usages2, nil).Once()

	req := &domain.ListSinkEndpointsReq{
		TenantID:  1,
		ProjectID: 2,
	}

	resp, err := s.model.ListSinkEndpoint(s.ctx, req)

	s.NoError(err)
	s.NotNil(resp)
	s.Equal(2, len(resp.SinkEndpoints))
	s.Equal(&endpoint1, resp.SinkEndpoints[0].Endpoint)
	s.NotNil(resp.SinkEndpoints[0].UsageInfo)
	s.Equal(1, len(resp.SinkEndpoints[0].UsageInfo.ChangefeedIds))
	s.Equal(&endpoint2, resp.SinkEndpoints[1].Endpoint)
	s.NotNil(resp.SinkEndpoints[1].UsageInfo)
	s.Equal(0, len(resp.SinkEndpoints[1].UsageInfo.ChangefeedIds))

	s.mockBizPLEndpointRepo.AssertExpectations(s.T())
	s.mockBizPLEndpointUsageRepo.AssertExpectations(s.T())
}
