package repository

import (
	"context"
	"errors"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	coremodel "security-center-backend/internal/model"
	caasmmodel "security-center-backend/internal/modules/caasm/model"
)

func TestGormAssetRepositoryPhase3SoftDeletedAssetsExcluded(t *testing.T) {
	ctx := context.Background()
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	deletedAt := time.Date(2026, time.May, 18, 8, 0, 0, 0, time.UTC)
	active := caasmmodel.Asset{
		Base:   coremodel.Base{ID: 10},
		Name:   "active-compute",
		Status: caasmmodel.AssetStatusActive,
	}
	deleted := caasmmodel.Asset{
		Base:   coremodel.Base{ID: 20, DeletedAt: &deletedAt},
		Name:   "deleted-compute",
		Status: caasmmodel.AssetStatusActive,
	}
	require.NoError(t, db.Create(&active).Error)
	require.NoError(t, db.Create(&deleted).Error)

	items, total, err := repo.List(ctx, AssetFilter{}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(1), total)
	require.Len(t, items, 1)
	require.Equal(t, active.ID, items[0].ID)

	_, err = repo.FindByID(ctx, deleted.ID)
	require.Error(t, err)
	require.True(t, errors.Is(err, gorm.ErrRecordNotFound))

	_, err = repo.Detail(ctx, deleted.ID)
	require.Error(t, err)
	require.True(t, errors.Is(err, gorm.ErrRecordNotFound))
}

func TestGormAssetRepositoryPhase3ListUsesStableIDPagination(t *testing.T) {
	ctx := context.Background()
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	for _, id := range []uint64{30, 10, 20} {
		require.NoError(t, db.Create(&caasmmodel.Asset{
			Base:   coremodel.Base{ID: id},
			Name:   "asset",
			Status: caasmmodel.AssetStatusActive,
		}).Error)
	}

	pageOne, total, err := repo.List(ctx, AssetFilter{}, 1, 2)
	require.NoError(t, err)
	require.Equal(t, int64(3), total)
	require.Len(t, pageOne, 2)
	require.Equal(t, []uint64{10, 20}, []uint64{pageOne[0].ID, pageOne[1].ID})

	pageTwo, total, err := repo.List(ctx, AssetFilter{}, 2, 2)
	require.NoError(t, err)
	require.Equal(t, int64(3), total)
	require.Len(t, pageTwo, 1)
	require.Equal(t, uint64(30), pageTwo[0].ID)
}

func TestGormAssetRepositoryPhase3SaveIPAssetNilInputReturnsError(t *testing.T) {
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	require.Error(t, repo.SaveIPAsset(context.Background(), nil))
}

func TestGormAssetRepositoryPhase3SaveIPAssetRequiredFieldsReturnError(t *testing.T) {
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	require.Error(t, repo.SaveIPAsset(context.Background(), &caasmmodel.IPAsset{IPAddress: "203.0.113.10"}))
	require.Error(t, repo.SaveIPAsset(context.Background(), &caasmmodel.IPAsset{AssetID: 1}))
}

func TestGormAssetRepositoryPhase3SaveIPAssetUpsertsWithoutDuplicate(t *testing.T) {
	ctx := context.Background()
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	asset := caasmmodel.Asset{
		Base:   coremodel.Base{ID: 100},
		Name:   "asset-with-ip",
		Status: caasmmodel.AssetStatusActive,
	}
	require.NoError(t, db.Create(&asset).Error)

	firstSeen := time.Date(2026, time.May, 18, 8, 0, 0, 0, time.UTC)
	lastSeen := firstSeen.Add(time.Minute)
	require.NoError(t, repo.SaveIPAsset(ctx, &caasmmodel.IPAsset{
		AssetID:     asset.ID,
		IPAddress:   "203.0.113.10",
		Hostname:    "old.example.test",
		IsPublic:    true,
		FirstSeenAt: &firstSeen,
		LastSeenAt:  &lastSeen,
	}))

	replacementFirstSeen := firstSeen.Add(24 * time.Hour)
	replacementLastSeen := firstSeen.Add(2 * time.Hour)
	require.NoError(t, repo.SaveIPAsset(ctx, &caasmmodel.IPAsset{
		AssetID:     asset.ID,
		IPAddress:   "203.0.113.10",
		Hostname:    "new.example.test",
		IsPublic:    false,
		FirstSeenAt: &replacementFirstSeen,
		LastSeenAt:  &replacementLastSeen,
	}))

	var saved []caasmmodel.IPAsset
	require.NoError(t, db.Where("asset_id = ? AND ip_address = ?", asset.ID, "203.0.113.10").Find(&saved).Error)
	require.Len(t, saved, 1)
	require.Equal(t, "new.example.test", saved[0].Hostname)
	require.False(t, saved[0].IsPublic)
	require.NotNil(t, saved[0].FirstSeenAt)
	require.True(t, saved[0].FirstSeenAt.Equal(firstSeen))
	require.NotNil(t, saved[0].LastSeenAt)
	require.True(t, saved[0].LastSeenAt.Equal(replacementLastSeen))
}

func TestGormAssetRepositoryPhase3SaveIPAssetConcurrentUpsertCollision(t *testing.T) {
	ctx := context.Background()
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	asset := caasmmodel.Asset{
		Base:   coremodel.Base{ID: 110},
		Name:   "asset-with-shared-ip",
		Status: caasmmodel.AssetStatusActive,
	}
	require.NoError(t, db.Create(&asset).Error)

	const workers = 8
	start := make(chan struct{})
	errs := make(chan error, workers)
	var wg sync.WaitGroup
	baseLastSeen := time.Date(2026, time.May, 18, 11, 0, 0, 0, time.UTC)

	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func(workerID int) {
			defer wg.Done()
			<-start
			errs <- repo.SaveIPAsset(ctx, &caasmmodel.IPAsset{
				AssetID:    asset.ID,
				IPAddress:  "203.0.113.40",
				Hostname:   "shared.example.test",
				IsPublic:   true,
				LastSeenAt: ptrTime(baseLastSeen.Add(time.Duration(workerID) * time.Second)),
			})
		}(i)
	}

	close(start)
	wg.Wait()
	close(errs)

	for err := range errs {
		require.NoError(t, err)
	}

	var count int64
	require.NoError(t, db.Model(&caasmmodel.IPAsset{}).
		Where("asset_id = ? AND ip_address = ?", asset.ID, "203.0.113.40").
		Count(&count).Error)
	require.Equal(t, int64(1), count)
}

func TestGormAssetRepositoryPhase3FindByNativeIDSkipsSoftDeletedAssets(t *testing.T) {
	ctx := context.Background()
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	accountID := uint64(700)
	deletedAt := time.Date(2026, time.May, 18, 8, 0, 0, 0, time.UTC)
	require.NoError(t, db.Create(&caasmmodel.Asset{
		Base:           coremodel.Base{ID: 701, DeletedAt: &deletedAt},
		CloudAccountID: &accountID,
		NativeID:       "native-deleted",
		Name:           "deleted",
		Status:         caasmmodel.AssetStatusActive,
	}).Error)

	_, err := repo.FindByNativeID(ctx, accountID, "native-deleted")
	require.Error(t, err)
	require.True(t, errors.Is(err, gorm.ErrRecordNotFound))
}

func TestGormAssetRepositoryPhase3SaveDoesNotResurfaceSoftDeletedAsset(t *testing.T) {
	ctx := context.Background()
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	accountID := uint64(710)
	deletedAt := time.Date(2026, time.May, 18, 8, 0, 0, 0, time.UTC)
	require.NoError(t, db.Create(&caasmmodel.Asset{
		Base:           coremodel.Base{ID: 711, DeletedAt: &deletedAt},
		CloudAccountID: &accountID,
		NativeID:       "native-soft-deleted",
		Name:           "deleted-old",
		Status:         caasmmodel.AssetStatusActive,
	}).Error)

	incoming := caasmmodel.Asset{
		CloudAccountID: &accountID,
		NativeID:       "native-soft-deleted",
		Name:           "active-new",
		Status:         caasmmodel.AssetStatusActive,
	}
	require.NoError(t, repo.Save(ctx, &incoming))
	require.NotZero(t, incoming.ID)
	require.NotEqual(t, uint64(711), incoming.ID)

	var deleted caasmmodel.Asset
	require.NoError(t, db.Unscoped().First(&deleted, 711).Error)
	require.NotNil(t, deleted.DeletedAt)

	var activeCount int64
	require.NoError(t, db.Model(&caasmmodel.Asset{}).
		Where("cloud_account_id = ? AND native_id = ? AND deleted_at IS NULL", accountID, "native-soft-deleted").
		Count(&activeCount).Error)
	require.Equal(t, int64(1), activeCount)
}

func TestGormAssetRepositoryPhase3SavePreservesManualOwnerFields(t *testing.T) {
	ctx := context.Background()
	db := newAssetRepositoryPhase3TestDB(t)
	repo := NewAssetRepository(db)

	accountID := uint64(720)
	ownerID := uint64(721)
	departmentID := "dept-721"
	require.NoError(t, db.Create(&caasmmodel.Asset{
		Base:            coremodel.Base{ID: 722},
		CloudAccountID:  &accountID,
		NativeID:        "native-owned",
		Name:            "owned-old",
		Status:          caasmmodel.AssetStatusActive,
		OwnerEmployeeID: &ownerID,
		DepartmentID:    &departmentID,
	}).Error)

	incoming := caasmmodel.Asset{
		CloudAccountID: &accountID,
		NativeID:       "native-owned",
		Name:           "owned-new",
		Status:         caasmmodel.AssetStatusActive,
	}
	require.NoError(t, repo.Save(ctx, &incoming))

	var saved caasmmodel.Asset
	require.NoError(t, db.First(&saved, 722).Error)
	require.NotNil(t, saved.OwnerEmployeeID)
	require.Equal(t, ownerID, *saved.OwnerEmployeeID)
	require.NotNil(t, saved.DepartmentID)
	require.Equal(t, departmentID, *saved.DepartmentID)
	require.Equal(t, "owned-new", saved.Name)
}

func newAssetRepositoryPhase3TestDB(t *testing.T) *gorm.DB {
	t.Helper()

	dbPath := filepath.Join(t.TempDir(), "asset_phase3.db")
	db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&caasmmodel.CloudAccount{},
		&caasmmodel.Asset{},
		&caasmmodel.IPAsset{},
		&caasmmodel.DomainAsset{},
		&caasmmodel.AssetRelationship{},
	))

	return db
}

func ptrTime(t time.Time) *time.Time {
	return &t
}
