package modules

import (
	"context"
	"strings"
	"testing"
	"time"

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

	caasmmodel "security-center-backend/internal/modules/caasm/model"
	scanmodel "security-center-backend/internal/modules/scan/model"
	scanrepository "security-center-backend/internal/modules/scan/repository"
	scanservice "security-center-backend/internal/modules/scan/service"
)

func TestCAASMPortTargetResolverAllPublicPortsReturnsEveryPublicPort(t *testing.T) {
	ctx := context.Background()
	db := newScanResolverTestDB(t)
	asset := createScanResolverAsset(t, db)
	first := createScanResolverPort(t, db, asset.ID, "8.8.8.8", 443, "tcp", "public")
	second := createScanResolverPort(t, db, asset.ID, "1.1.1.1", 8443, "tcp", "public")
	third := createScanResolverPort(t, db, asset.ID, "9.9.9.9", 53, "udp", "public")
	createScanResolverPort(t, db, asset.ID, "10.0.0.1", 22, "tcp", "private")

	resolver := &caasmPortTargetResolver{db: db}
	candidates, err := resolver.ResolveTargets(ctx, scanservice.ResolveTargetsRequest{
		Source:      scanservice.SourceCAASMPorts,
		TargetScope: scanservice.TargetScopeAllPublicPorts,
		Limit:       10,
	})

	require.NoError(t, err)
	require.Len(t, candidates, 3)
	require.Equal(t, first.ID, *candidates[0].CAASMPortID)
	require.Equal(t, second.ID, *candidates[1].CAASMPortID)
	require.Equal(t, third.ID, *candidates[2].CAASMPortID)
	require.Equal(t, "8.8.8.8", candidates[0].PublicIP)
	require.Equal(t, 443, candidates[0].Port)
	require.Equal(t, "tcp", candidates[0].Transport)
	require.Equal(t, "udp", candidates[2].Transport)
	require.Equal(t, "platform", candidates[0].Owner)
}

func TestCAASMPortTargetResolverAllPublicPortsHonorsLimit(t *testing.T) {
	ctx := context.Background()
	db := newScanResolverTestDB(t)
	asset := createScanResolverAsset(t, db)
	first := createScanResolverPort(t, db, asset.ID, "8.8.8.8", 443, "tcp", "public")
	createScanResolverPort(t, db, asset.ID, "1.1.1.1", 8443, "tcp", "public")

	resolver := &caasmPortTargetResolver{db: db}
	candidates, err := resolver.ResolveTargets(ctx, scanservice.ResolveTargetsRequest{
		Source:      scanservice.SourceCAASMPorts,
		TargetScope: scanservice.TargetScopeAllPublicPorts,
		Limit:       1,
	})

	require.NoError(t, err)
	require.Len(t, candidates, 1)
	require.Equal(t, first.ID, *candidates[0].CAASMPortID)
}

func TestCAASMPortTargetResolverSelectedPortsPreservesOrderAndMissingIDs(t *testing.T) {
	ctx := context.Background()
	db := newScanResolverTestDB(t)
	asset := createScanResolverAsset(t, db)
	first := createScanResolverPort(t, db, asset.ID, "8.8.8.8", 443, "tcp", "public")
	second := createScanResolverPort(t, db, asset.ID, "1.1.1.1", 8443, "tcp", "public")

	resolver := &caasmPortTargetResolver{db: db}
	candidates, err := resolver.ResolveTargets(ctx, scanservice.ResolveTargetsRequest{
		Source:      scanservice.SourceCAASMPorts,
		TargetScope: scanservice.TargetScopeSelectedPorts,
		TargetIDs:   []uint64{second.ID, 999999, first.ID},
		Limit:       10,
	})

	require.NoError(t, err)
	require.Len(t, candidates, 3)
	require.Equal(t, second.ID, *candidates[0].CAASMPortID)
	require.Equal(t, uint64(999999), *candidates[1].CAASMPortID)
	require.Empty(t, candidates[1].PublicIP)
	require.Equal(t, first.ID, *candidates[2].CAASMPortID)
}

func TestScanRepositoryAdapterAggregateCampaignTreatsCanceledChildrenAsTerminal(t *testing.T) {
	ctx := context.Background()
	db := newScanAdapterTestDB(t)
	repo := scanrepository.NewScanRepository(db)
	adapter := &scanRepositoryAdapter{
		db:   db,
		repo: repo,
		now: func() time.Time {
			return time.Date(2026, 6, 10, 12, 0, 0, 0, time.UTC)
		},
	}
	campaign := scanmodel.Campaign{
		RequestID:   "campaign-cancel-test",
		Source:      scanservice.SourceCAASMPorts,
		TargetScope: scanservice.TargetScopeAllPublicPorts,
		Status:      scanmodel.CampaignStatusRunning,
	}
	require.NoError(t, repo.CreateCampaign(ctx, &campaign))
	require.NoError(t, db.Create(&scanmodel.Batch{
		RequestID:       "campaign-cancel-test-part-000001",
		CampaignID:      &campaign.ID,
		Source:          scanservice.SourceCAASMPorts,
		Status:          scanmodel.BatchStatusCanceled,
		AcceptedTargets: 10,
		SkippedJobs:     10,
	}).Error)

	aggregated, err := adapter.AggregateCampaign(ctx, campaign.ID)

	require.NoError(t, err)
	require.Equal(t, "canceled", aggregated.Status)
	require.Equal(t, 1, aggregated.BatchCount)
	require.Equal(t, 1, aggregated.CompletedBatches)
	require.NotNil(t, aggregated.CompletedAt)
}

func TestScanRepositoryAdapterApplyObservationsClearsTerminalQueueCounters(t *testing.T) {
	ctx := context.Background()
	completedAt := time.Date(2026, 6, 10, 12, 24, 45, 0, time.UTC)
	db := newScanAdapterTestDB(t)
	repo := scanrepository.NewScanRepository(db)
	adapter := &scanRepositoryAdapter{
		db:   db,
		repo: repo,
		now: func() time.Time {
			return time.Date(2026, 6, 10, 12, 25, 0, 0, time.UTC)
		},
	}
	campaign := scanmodel.Campaign{
		RequestID:       "campaign-callback-test",
		Source:          scanservice.SourceCAASMPorts,
		TargetScope:     scanservice.TargetScopeAllPublicPorts,
		Status:          scanmodel.CampaignStatusRunning,
		TargetCount:     3,
		AcceptedTargets: 3,
	}
	require.NoError(t, repo.CreateCampaign(ctx, &campaign))
	batch := scanmodel.Batch{
		RequestID:       "campaign-callback-test-part-000001",
		ScannerBatchID:  nullableString("scanner-callback-test"),
		CampaignID:      &campaign.ID,
		Source:          scanservice.SourceCAASMPorts,
		Status:          scanmodel.BatchStatusRunning,
		TargetCount:     3,
		AcceptedTargets: 3,
		QueuedJobs:      3,
	}
	require.NoError(t, db.Create(&batch).Error)

	err := adapter.ApplyObservations(ctx, scanservice.ApplyObservationsInput{
		Batch:       *mapBatch(&batch),
		CompletedAt: &completedAt,
		Status:      string(scanmodel.BatchStatusCompleted),
		Observations: []scanservice.AppliedObservation{
			{PublicIP: "45.33.32.156", Port: 80, Transport: "tcp", Result: string(scanmodel.ObservationResultConfirmedOpen), ReasonCode: "tcp_connected"},
			{PublicIP: "45.33.32.156", Port: 22, Transport: "tcp", Result: string(scanmodel.ObservationResultUnknownFiltered), ReasonCode: "connect_timeout"},
			{PublicIP: "45.33.32.156", Port: 443, Transport: "tcp", Result: string(scanmodel.ObservationResultSkipped), ReasonCode: "batch_canceled"},
		},
	})
	require.NoError(t, err)

	var updatedBatch scanmodel.Batch
	require.NoError(t, db.First(&updatedBatch, batch.ID).Error)
	require.Equal(t, scanmodel.BatchStatusCompleted, updatedBatch.Status)
	require.Zero(t, updatedBatch.QueuedJobs)
	require.Zero(t, updatedBatch.RunningJobs)
	require.Equal(t, 2, updatedBatch.SucceededJobs)
	require.Zero(t, updatedBatch.FailedJobs)
	require.Equal(t, 1, updatedBatch.SkippedJobs)

	aggregated, err := adapter.AggregateCampaign(ctx, campaign.ID)
	require.NoError(t, err)
	require.Equal(t, "completed", aggregated.Status)
	require.Zero(t, aggregated.QueuedJobs)
	require.Zero(t, aggregated.RunningJobs)
	require.Equal(t, 2, aggregated.SucceededJobs)
	require.Equal(t, 1, aggregated.SkippedJobs)
	require.NotNil(t, aggregated.CompletedAt)
}

func TestScanRepositoryAdapterStatsCountsCanceledDrainingBatchAsActive(t *testing.T) {
	ctx := context.Background()
	db := newScanAdapterTestDB(t)
	repo := scanrepository.NewScanRepository(db)
	adapter := &scanRepositoryAdapter{db: db, repo: repo}
	lastSyncedAt := time.Date(2026, 6, 10, 12, 30, 0, 0, time.UTC)
	require.NoError(t, db.Create(&scanmodel.Batch{
		RequestID:      "batch-cancel-draining",
		Source:         scanservice.SourceCAASMPorts,
		Status:         scanmodel.BatchStatusCanceled,
		ScannerBatchID: nullableString("scanner-draining"),
		RunningJobs:    1,
		SkippedJobs:    3,
		LastSyncedAt:   &lastSyncedAt,
	}).Error)
	require.NoError(t, db.Create(&scanmodel.Batch{
		RequestID:      "batch-completed",
		Source:         scanservice.SourceCAASMPorts,
		Status:         scanmodel.BatchStatusCompleted,
		ScannerBatchID: nullableString("scanner-completed"),
		SucceededJobs:  2,
	}).Error)

	stats, err := adapter.GetStats(ctx)

	require.NoError(t, err)
	require.Equal(t, int64(1), stats.ActiveBatches)
	require.Equal(t, int64(1), stats.RunningJobs)
	require.Zero(t, stats.SucceededJobs)
	require.Equal(t, int64(3), stats.SkippedJobs)
	require.NotNil(t, stats.LatestScannerSyncAt)
	require.True(t, stats.LatestScannerSyncAt.Equal(lastSyncedAt))
}

func newScanResolverTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
	db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&caasmmodel.CloudAccount{},
		&caasmmodel.Asset{},
		&caasmmodel.IPPortIndex{},
	))
	return db
}

func newScanAdapterTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
	db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&scanmodel.Campaign{},
		&scanmodel.Batch{},
		&scanmodel.Target{},
		&scanmodel.Observation{},
		&scanmodel.CallbackReceipt{},
	))
	return db
}

func createScanResolverAsset(t *testing.T, db *gorm.DB) caasmmodel.Asset {
	t.Helper()
	asset := caasmmodel.Asset{
		NativeID:    "asset-native-1",
		Name:        "asset-1",
		AssetType:   caasmmodel.AssetTypeCompute,
		Service:     "platform",
		Criticality: "high",
		Status:      caasmmodel.AssetStatusActive,
		Environment: "prod",
		Region:      "us-east-1",
		Provider:    "aws",
		FirstSeenAt: timePtr(time.Date(2026, 6, 9, 10, 0, 0, 0, time.UTC)),
		LastSeenAt:  timePtr(time.Date(2026, 6, 9, 10, 0, 0, 0, time.UTC)),
	}
	require.NoError(t, db.Create(&asset).Error)
	return asset
}

func createScanResolverPort(t *testing.T, db *gorm.DB, assetID uint64, ip string, port uint32, protocol string, exposure string) caasmmodel.IPPortIndex {
	t.Helper()
	entry := caasmmodel.IPPortIndex{
		AssetID:           assetID,
		IPAddress:         ip,
		Port:              port,
		Protocol:          protocol,
		ObservationSource: caasmmodel.PortObservationSourceSecurityGroup,
		ServiceName:       "https",
		Exposure:          exposure,
		Status:            "open",
		LastSeenAt:        time.Date(2026, 6, 9, 10, 0, 0, 0, time.UTC),
	}
	require.NoError(t, db.Create(&entry).Error)
	return entry
}

func timePtr(value time.Time) *time.Time {
	return &value
}
