package repository

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

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

	coremodel "security-center-backend/internal/model"
	scanmodel "security-center-backend/internal/modules/scan/model"
)

func TestGormScanRepositoryUpsertObservationUsesScannerTargetFallback(t *testing.T) {
	ctx := context.Background()
	db := newScanRepositoryTestDB(t)
	repo := NewScanRepository(db)
	batch := createScanRepositoryTestBatch(t, ctx, repo, "request-scanner-target")

	scannerTargetID := "scanner-target-1"
	require.NoError(t, repo.UpsertObservation(ctx, &scanmodel.Observation{
		BatchID:         batch.ID,
		ScannerTargetID: &scannerTargetID,
		PublicIP:        "203.0.113.10",
		Port:            443,
		Transport:       "tcp",
		Result:          scanmodel.ObservationResultConfirmedOpen,
		ReasonCode:      "tcp_connected",
		ConnectMS:       10,
		ObservedAt:      time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC),
	}))

	require.NoError(t, repo.UpsertObservation(ctx, &scanmodel.Observation{
		BatchID:         batch.ID,
		ScannerTargetID: &scannerTargetID,
		PublicIP:        "203.0.113.10",
		Port:            443,
		Transport:       "tcp",
		Result:          scanmodel.ObservationResultConfirmedClosed,
		ReasonCode:      "connection_refused",
		ConnectMS:       20,
		ObservedAt:      time.Date(2026, 6, 8, 10, 1, 0, 0, time.UTC),
	}))

	observations, total, err := repo.ListObservations(ctx, ObservationListFilter{BatchID: batch.ID}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(1), total)
	require.Len(t, observations, 1)
	require.Equal(t, scanmodel.ObservationResultConfirmedClosed, observations[0].Result)
	require.Equal(t, int64(20), observations[0].ConnectMS)
}

func TestGormScanRepositoryUpsertObservationUsesEndpointFallback(t *testing.T) {
	ctx := context.Background()
	db := newScanRepositoryTestDB(t)
	repo := NewScanRepository(db)
	batch := createScanRepositoryTestBatch(t, ctx, repo, "request-endpoint")

	require.NoError(t, repo.UpsertObservation(ctx, &scanmodel.Observation{
		BatchID:    batch.ID,
		PublicIP:   "203.0.113.11",
		Port:       8443,
		Transport:  "tcp",
		Result:     scanmodel.ObservationResultUnknownFiltered,
		ReasonCode: "timeout",
		ObservedAt: time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC),
	}))
	require.NoError(t, repo.UpsertObservation(ctx, &scanmodel.Observation{
		BatchID:    batch.ID,
		PublicIP:   "203.0.113.11",
		Port:       8443,
		Transport:  "tcp",
		Result:     scanmodel.ObservationResultConfirmedOpen,
		ReasonCode: "tcp_connected",
		ObservedAt: time.Date(2026, 6, 8, 10, 2, 0, 0, time.UTC),
	}))

	observations, total, err := repo.ListObservations(ctx, ObservationListFilter{BatchID: batch.ID}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(1), total)
	require.Len(t, observations, 1)
	require.Equal(t, scanmodel.ObservationResultConfirmedOpen, observations[0].Result)
}

func TestGormScanRepositoryListLatestPortExposuresKeepsConfirmedOpenAfterLaterNonOpen(t *testing.T) {
	ctx := context.Background()
	db := newScanRepositoryTestDB(t)
	repo := NewScanRepository(db)
	firstBatch := createScanRepositoryTestBatch(t, ctx, repo, "request-open")
	secondBatch := createScanRepositoryTestBatch(t, ctx, repo, "request-closed")

	owner := "platform"
	environment := "prod"
	targetID := createScanRepositoryTestTarget(t, db, firstBatch.ID, "203.0.113.12", 443, owner, environment)
	require.NoError(t, repo.UpsertObservation(ctx, &scanmodel.Observation{
		BatchID:    firstBatch.ID,
		TargetID:   &targetID,
		PublicIP:   "203.0.113.12",
		Port:       443,
		Transport:  "tcp",
		Result:     scanmodel.ObservationResultConfirmedOpen,
		ReasonCode: "tcp_connected",
		ObservedAt: time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC),
	}))
	require.NoError(t, repo.UpsertObservation(ctx, &scanmodel.Observation{
		BatchID:    secondBatch.ID,
		PublicIP:   "203.0.113.12",
		Port:       443,
		Transport:  "tcp",
		Result:     scanmodel.ObservationResultSkipped,
		ReasonCode: "batch_canceled",
		ObservedAt: time.Date(2026, 6, 8, 11, 0, 0, 0, time.UTC),
	}))
	require.NoError(t, repo.UpsertObservation(ctx, &scanmodel.Observation{
		BatchID:    secondBatch.ID,
		PublicIP:   "203.0.113.13",
		Port:       22,
		Transport:  "tcp",
		Result:     scanmodel.ObservationResultConfirmedOpen,
		ReasonCode: "tcp_connected",
		ObservedAt: time.Date(2026, 6, 8, 11, 5, 0, 0, time.UTC),
	}))

	rows, total, err := repo.ListLatestPortExposures(ctx, PortExposureFilter{}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(2), total)
	require.Len(t, rows, 2)
	require.Equal(t, "203.0.113.13", rows[0].PublicIP)
	require.Equal(t, string(scanmodel.ObservationResultConfirmedOpen), rows[0].Result)
	require.Equal(t, "203.0.113.12", rows[1].PublicIP)
	require.Equal(t, string(scanmodel.ObservationResultConfirmedOpen), rows[1].Result)
}

func TestGormScanRepositoryListCallbackReceiptsByBatch(t *testing.T) {
	ctx := context.Background()
	db := newScanRepositoryTestDB(t)
	repo := NewScanRepository(db)
	batch := createScanRepositoryTestBatch(t, ctx, repo, "request-callback")
	otherBatch := createScanRepositoryTestBatch(t, ctx, repo, "request-other-callback")

	require.NoError(t, repo.CreateCallbackReceipt(ctx, &scanmodel.CallbackReceipt{
		IdempotencyKey:      "idem-1",
		LocalBatchID:        &batch.ID,
		PayloadSHA256:       "abc",
		HeaderPayloadSHA256: "abc",
		Status:              scanmodel.CallbackReceiptStatusApplied,
		HTTPStatus:          200,
		ReceivedAt:          time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC),
	}))
	require.NoError(t, repo.CreateCallbackReceipt(ctx, &scanmodel.CallbackReceipt{
		IdempotencyKey:      "idem-2",
		LocalBatchID:        &otherBatch.ID,
		PayloadSHA256:       "def",
		HeaderPayloadSHA256: "def",
		Status:              scanmodel.CallbackReceiptStatusFailed,
		HTTPStatus:          500,
		ReceivedAt:          time.Date(2026, 6, 8, 12, 1, 0, 0, time.UTC),
	}))

	receipts, total, err := repo.ListCallbackReceipts(ctx, CallbackReceiptListFilter{LocalBatchID: &batch.ID}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(1), total)
	require.Len(t, receipts, 1)
	require.Equal(t, "idem-1", receipts[0].IdempotencyKey)
}

func TestGormScanRepositoryActiveStatusFilterIncludesCancelableAndDrainingWork(t *testing.T) {
	ctx := context.Background()
	db := newScanRepositoryTestDB(t)
	repo := NewScanRepository(db)

	require.NoError(t, db.Create(&scanmodel.Batch{
		RequestID:   "running-batch",
		Status:      scanmodel.BatchStatusRunning,
		RunningJobs: 2,
		Metadata:    coremodel.JSONB{},
	}).Error)
	require.NoError(t, db.Create(&scanmodel.Batch{
		RequestID:   "draining-batch",
		Status:      scanmodel.BatchStatusCanceled,
		RunningJobs: 1,
		Metadata:    coremodel.JSONB{},
	}).Error)
	require.NoError(t, db.Create(&scanmodel.Batch{
		RequestID: "done-batch",
		Status:    scanmodel.BatchStatusCompleted,
		Metadata:  coremodel.JSONB{},
	}).Error)

	batches, total, err := repo.ListBatches(ctx, BatchListFilter{Status: "active"}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(2), total)
	require.Len(t, batches, 2)

	require.NoError(t, db.Create(&scanmodel.Campaign{
		RequestID:   "running-campaign",
		Status:      scanmodel.CampaignStatusRunning,
		RunningJobs: 2,
		Metadata:    coremodel.JSONB{},
	}).Error)
	require.NoError(t, db.Create(&scanmodel.Campaign{
		RequestID:   "draining-campaign",
		Status:      scanmodel.CampaignStatusCanceled,
		RunningJobs: 1,
		Metadata:    coremodel.JSONB{},
	}).Error)
	require.NoError(t, db.Create(&scanmodel.Campaign{
		RequestID: "done-campaign",
		Status:    scanmodel.CampaignStatusCompleted,
		Metadata:  coremodel.JSONB{},
	}).Error)

	campaigns, campaignTotal, err := repo.ListCampaigns(ctx, CampaignListFilter{Status: "active"}, 1, 20)
	require.NoError(t, err)
	require.Equal(t, int64(2), campaignTotal)
	require.Len(t, campaigns, 2)
}

func newScanRepositoryTestDB(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 createScanRepositoryTestBatch(t *testing.T, ctx context.Context, repo ScanRepository, requestID string) *scanmodel.Batch {
	t.Helper()
	batch := scanmodel.Batch{
		RequestID: requestID,
		Status:    scanmodel.BatchStatusCompleted,
		Metadata:  coremodel.JSONB{},
	}
	require.NoError(t, repo.CreateBatch(ctx, &batch, nil))
	return &batch
}

func createScanRepositoryTestTarget(t *testing.T, db *gorm.DB, batchID uint64, publicIP string, port int, owner, environment string) uint64 {
	t.Helper()
	target := scanmodel.Target{
		BatchID:        batchID,
		ScannerAssetID: "asset-1",
		PublicIP:       publicIP,
		Port:           port,
		Transport:      "tcp",
		TargetStatus:   scanmodel.TargetStatusObserved,
		Owner:          &owner,
		Environment:    &environment,
		Metadata:       coremodel.JSONB{},
	}
	require.NoError(t, db.Create(&target).Error)
	return target.ID
}
