package service

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

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

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

func TestPersistSyncPayloadPhase3TagFailureReturnsPersistError(t *testing.T) {
	assetRepo := &mocks.MockAssetRepository{}
	assetRepo.On("SaveAssetSnapshot", mock.Anything, mock.MatchedBy(func(asset *caasmmodel.Asset) bool {
		return asset.NativeID == "asset-with-tags"
	})).Run(func(args mock.Arguments) {
		args.Get(1).(*caasmmodel.Asset).ID = 1001
	}).Return(nil)
	events := &phase2EventRepo{}
	svc := &CloudAccountService{
		assets: assetRepo,
		tags:   phase3TagRepo{replaceErr: errors.New("tag store down")},
		events: events,
	}

	err := svc.persistSyncPayload(context.Background(), phase3Job(), &caasmmodel.IngestPayload{
		CloudAccountID: 9001,
		Assets: []caasmmodel.Asset{{
			NativeID: "asset-with-tags",
			Name:     "asset-with-tags",
			Metadata: coremodel.JSONB{"tags": map[string]interface{}{"env": "prod"}},
		}},
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "asset.tags.replace")
	requirePersistFailureEvent(t, events, 1)
	assetRepo.AssertExpectations(t)
}

func TestPersistSyncPayloadPhase3DNSLoadFailureReturnsPersistError(t *testing.T) {
	assetRepo := &mocks.MockAssetRepository{}
	assetRepo.On("SaveDomainAssetSnapshot", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
		args.Get(1).(*caasmmodel.DomainAsset).ID = 2001
	}).Return(nil)
	events := &phase2EventRepo{}
	svc := &CloudAccountService{
		assets:     assetRepo,
		dnsRecords: phase3DNSRepo{getErr: errors.New("dns read failed")},
		events:     events,
	}

	err := svc.persistSyncPayload(context.Background(), phase3Job(), &caasmmodel.IngestPayload{
		CloudAccountID: 9002,
		DomainAssets: []caasmmodel.DomainAsset{{
			AssetID:     42,
			DomainName:  "example.test",
			DomainClass: caasmmodel.DomainClassAuthoritativeZone,
			DNSRecords:  coremodel.JSONB{"records": []interface{}{}},
		}},
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "dns_records.load_existing")
	requirePersistFailureEvent(t, events, 1)
	assetRepo.AssertExpectations(t)
}

func TestPersistSyncPayloadPhase3DNSReplaceFailureReturnsPersistError(t *testing.T) {
	assetRepo := &mocks.MockAssetRepository{}
	assetRepo.On("SaveDomainAssetSnapshot", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
		args.Get(1).(*caasmmodel.DomainAsset).ID = 2101
	}).Return(nil)
	events := &phase2EventRepo{}
	svc := &CloudAccountService{
		assets:     assetRepo,
		dnsRecords: phase3DNSRepo{replaceErr: errors.New("dns replace failed")},
		events:     events,
	}

	err := svc.persistSyncPayload(context.Background(), phase3Job(), &caasmmodel.IngestPayload{
		CloudAccountID: 9003,
		DomainAssets: []caasmmodel.DomainAsset{{
			AssetID:     43,
			DomainName:  "replace.example.test",
			DomainClass: caasmmodel.DomainClassAuthoritativeZone,
			DNSRecords:  coremodel.JSONB{"records": []interface{}{}},
		}},
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "dns_records.replace")
	requirePersistFailureEvent(t, events, 1)
	assetRepo.AssertExpectations(t)
}

func TestPersistSyncPayloadPhase3PortFailureReturnsPersistError(t *testing.T) {
	assetRepo := &mocks.MockAssetRepository{}
	portRepo := &mocks.MockIPPortIndexRepository{}
	portRepo.On("Upsert", mock.Anything, mock.MatchedBy(func(port *caasmmodel.IPPortIndex) bool {
		return port.AssetID == 55 && port.IPAddress == "203.0.113.55"
	})).Return(errors.New("port write failed"))
	events := &phase2EventRepo{}
	svc := &CloudAccountService{
		assets: assetRepo,
		ports:  portRepo,
		events: events,
	}

	err := svc.persistSyncPayload(context.Background(), phase3Job(), &caasmmodel.IngestPayload{
		CloudAccountID: 9004,
		Ports: []caasmmodel.IPPortIndex{{
			AssetID:   55,
			IPAddress: "203.0.113.55",
			Port:      443,
			Protocol:  "tcp",
			Exposure:  "public",
			Status:    "open",
		}},
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "port.upsert")
	requirePersistFailureEvent(t, events, 1)
	portRepo.AssertExpectations(t)
}

func TestPersistSyncPayloadPhase3HistoryFailureReturnsPersistError(t *testing.T) {
	assetRepo := &mocks.MockAssetRepository{}
	assetRepo.On("SaveAssetSnapshot", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
		args.Get(1).(*caasmmodel.Asset).ID = 3001
	}).Return(nil)
	events := &phase2EventRepo{}
	svc := &CloudAccountService{
		assets:  assetRepo,
		history: phase3HistoryRepo{err: errors.New("history write failed")},
		events:  events,
	}

	err := svc.persistSyncPayload(context.Background(), phase3Job(), &caasmmodel.IngestPayload{
		CloudAccountID: 9005,
		Assets: []caasmmodel.Asset{{
			NativeID: "asset-history",
			Name:     "asset-history",
		}},
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "history.asset_upsert")
	requirePersistFailureEvent(t, events, 1)
	assetRepo.AssertExpectations(t)
}

func TestPersistPayloadAndMarkStalePhase3SkipsStaleMarkingOnPersistFailure(t *testing.T) {
	assetRepo := &mocks.MockAssetRepository{}
	assetRepo.On("SaveAssetSnapshot", mock.Anything, mock.Anything).Return(errors.New("asset write failed"))
	events := &phase2EventRepo{}
	svc := &CloudAccountService{
		assets: assetRepo,
		events: events,
	}

	err := svc.persistPayloadAndMarkStale(
		context.Background(),
		phase3Job(),
		&caasmmodel.CloudAccount{Base: coremodel.Base{ID: 99}},
		&caasmmodel.IngestPayload{
			CloudAccountID: 99,
			Assets: []caasmmodel.Asset{{
				NativeID: "asset-fails",
				Name:     "asset-fails",
			}},
		},
		time.Now(),
	)

	require.Error(t, err)
	require.Contains(t, err.Error(), "asset.save")
	assetRepo.AssertNotCalled(t, "SoftDeleteStale", mock.Anything, mock.Anything, mock.Anything)
	requirePersistFailureEvent(t, events, 1)
	assetRepo.AssertExpectations(t)
}

func phase3Job() *caasmmodel.SyncJob {
	return &caasmmodel.SyncJob{Base: coremodel.Base{ID: 8001}}
}

func requirePersistFailureEvent(t *testing.T, repo *phase2EventRepo, failureCount int) {
	t.Helper()
	require.NotNil(t, repo.created)
	require.Equal(t, "Persist failed with write errors", repo.created.Message)
	require.Equal(t, caasmmodel.SyncJobEventType("error"), repo.created.Type)
	require.Equal(t, failureCount, repo.created.Metadata["failure_count"])
	require.Equal(t, "persist", repo.created.Metadata["phase"])
	require.True(t, strings.Contains(repo.created.Metadata["error"].(string), "persist"))
}

type phase3TagRepo struct {
	replaceErr error
}

func (r phase3TagRepo) ReplaceTags(context.Context, uint64, map[string]string) error {
	return r.replaceErr
}

func (r phase3TagRepo) GetTags(context.Context, uint64) (map[string]string, error) {
	return map[string]string{}, nil
}

type phase3DNSRepo struct {
	getErr     error
	replaceErr error
}

func (r phase3DNSRepo) List(context.Context, repository.DNSRecordFilter, int, int) ([]caasmmodel.DNSRecord, int64, error) {
	return nil, 0, nil
}

func (r phase3DNSRepo) GetByDomain(context.Context, uint64) ([]caasmmodel.DNSRecord, error) {
	return nil, r.getErr
}

func (r phase3DNSRepo) ReplaceForDomain(context.Context, uint64, []caasmmodel.DNSRecord) error {
	return r.replaceErr
}

func (r phase3DNSRepo) UpdateOwner(context.Context, uint64, string, string, *uint64, *bool, *string) error {
	return nil
}

type phase3HistoryRepo struct {
	err error
}

func (r phase3HistoryRepo) Create(context.Context, *caasmmodel.AssetHistory) error {
	return r.err
}
