package service

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/require"

	coremodel "security-center-backend/internal/model"
	vulnmodel "security-center-backend/internal/modules/vuln/model"
)

type attachmentRepoStub struct {
	items        map[uint64]*vulnmodel.VulnerabilityAttachment
	attachErr    error
	skipAttach   bool
	attachedIDs  []uint64
	attachUserID uint64
	attachVulnID uint64
}

func (r *attachmentRepoStub) Create(ctx context.Context, attachment *vulnmodel.VulnerabilityAttachment) error {
	return nil
}

func (r *attachmentRepoStub) FindByID(ctx context.Context, id uint64) (*vulnmodel.VulnerabilityAttachment, error) {
	item, ok := r.items[id]
	if !ok || item == nil {
		return nil, errors.New("not found")
	}
	copy := *item
	return &copy, nil
}

func (r *attachmentRepoStub) UpdateFields(ctx context.Context, id uint64, fields map[string]interface{}) error {
	return nil
}

func (r *attachmentRepoStub) AttachToVulnerability(ctx context.Context, uploaderID uint64, vulnID uint64, attachmentIDs []uint64) error {
	r.attachUserID = uploaderID
	r.attachVulnID = vulnID
	r.attachedIDs = append([]uint64(nil), attachmentIDs...)
	if r.attachErr != nil {
		return r.attachErr
	}
	if r.skipAttach {
		return nil
	}
	for _, id := range attachmentIDs {
		item := r.items[id]
		if item == nil {
			continue
		}
		boundID := uint64(0)
		if item.VulnerabilityID != nil {
			boundID = *item.VulnerabilityID
		}
		if item.UploaderID == uploaderID && item.Status == vulnmodel.AttachmentStatusReady && boundID == 0 {
			boundID := vulnID
			item.VulnerabilityID = &boundID
		}
	}
	return nil
}

func newAttachmentService(repo *attachmentRepoStub) *Service {
	return New(nil, nil, nil, nil, nil, nil, repo, nil, nil, nil, nil)
}

func attachment(id uint64, uploaderID uint64, status vulnmodel.VulnerabilityAttachmentStatus, vulnID *uint64) *vulnmodel.VulnerabilityAttachment {
	return &vulnmodel.VulnerabilityAttachment{
		Base:            coremodel.Base{ID: id},
		UploaderID:      uploaderID,
		Status:          status,
		VulnerabilityID: vulnID,
	}
}

func uint64Ptr(value uint64) *uint64 {
	return &value
}

func TestValidateAndAttachDescriptionAttachmentsBindsUnboundReadyAttachment(t *testing.T) {
	repo := &attachmentRepoStub{
		items: map[uint64]*vulnmodel.VulnerabilityAttachment{
			11: attachment(11, 7, vulnmodel.AttachmentStatusReady, nil),
		},
	}
	svc := newAttachmentService(repo)

	err := svc.ValidateAndAttachDescriptionAttachments(context.Background(), 7, 99, []uint64{11})

	require.NoError(t, err)
	require.Equal(t, uint64(7), repo.attachUserID)
	require.Equal(t, uint64(99), repo.attachVulnID)
	require.Equal(t, []uint64{11}, repo.attachedIDs)
	require.NotNil(t, repo.items[11].VulnerabilityID)
	require.Equal(t, uint64(99), *repo.items[11].VulnerabilityID)
}

func TestValidateAndAttachDescriptionAttachmentsAllowsAlreadyBoundToSameVulnerability(t *testing.T) {
	repo := &attachmentRepoStub{
		items: map[uint64]*vulnmodel.VulnerabilityAttachment{
			11: attachment(11, 8, vulnmodel.AttachmentStatusReady, uint64Ptr(99)),
		},
	}
	svc := newAttachmentService(repo)

	err := svc.ValidateAndAttachDescriptionAttachments(context.Background(), 7, 99, []uint64{11, 11})

	require.NoError(t, err)
	require.Equal(t, []uint64{11}, repo.attachedIDs)
	require.Equal(t, uint64(99), *repo.items[11].VulnerabilityID)
}

func TestValidateDescriptionAttachmentsAllowsAlreadyBoundToSameVulnerability(t *testing.T) {
	repo := &attachmentRepoStub{
		items: map[uint64]*vulnmodel.VulnerabilityAttachment{
			11: attachment(11, 8, vulnmodel.AttachmentStatusReady, uint64Ptr(99)),
		},
	}
	svc := newAttachmentService(repo)

	err := svc.ValidateDescriptionAttachments(context.Background(), 7, 99, []uint64{11})

	require.NoError(t, err)
	require.Empty(t, repo.attachedIDs)
}

func TestValidateAndAttachDescriptionAttachmentsRejectsInvalidReferences(t *testing.T) {
	tests := []struct {
		name string
		item *vulnmodel.VulnerabilityAttachment
	}{
		{name: "missing", item: nil},
		{name: "other uploader", item: attachment(11, 8, vulnmodel.AttachmentStatusReady, nil)},
		{name: "pending", item: attachment(11, 7, vulnmodel.AttachmentStatusPending, nil)},
		{name: "bound to other vulnerability", item: attachment(11, 7, vulnmodel.AttachmentStatusReady, uint64Ptr(100))},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			repo := &attachmentRepoStub{items: map[uint64]*vulnmodel.VulnerabilityAttachment{}}
			if tt.item != nil {
				repo.items[11] = tt.item
			}
			svc := newAttachmentService(repo)

			err := svc.ValidateAndAttachDescriptionAttachments(context.Background(), 7, 99, []uint64{11})

			require.ErrorIs(t, err, ErrInvalidAttachmentReference)
			require.Empty(t, repo.attachedIDs)
		})
	}
}

func TestValidateAndAttachDescriptionAttachmentsRechecksAfterAttach(t *testing.T) {
	t.Run("attach error", func(t *testing.T) {
		repo := &attachmentRepoStub{
			items: map[uint64]*vulnmodel.VulnerabilityAttachment{
				11: attachment(11, 7, vulnmodel.AttachmentStatusReady, nil),
			},
			attachErr: errors.New("database unavailable"),
		}
		svc := newAttachmentService(repo)

		err := svc.ValidateAndAttachDescriptionAttachments(context.Background(), 7, 99, []uint64{11})

		require.EqualError(t, err, "database unavailable")
	})

	t.Run("no rows changed", func(t *testing.T) {
		repo := &attachmentRepoStub{
			items: map[uint64]*vulnmodel.VulnerabilityAttachment{
				11: attachment(11, 7, vulnmodel.AttachmentStatusReady, nil),
			},
			skipAttach: true,
		}
		svc := newAttachmentService(repo)

		err := svc.ValidateAndAttachDescriptionAttachments(context.Background(), 7, 99, []uint64{11})

		require.ErrorIs(t, err, ErrInvalidAttachmentReference)
	})
}

func TestValidateDescriptionAttachmentsRejectsCreateReferenceAlreadyBoundToAnotherVulnerability(t *testing.T) {
	repo := &attachmentRepoStub{
		items: map[uint64]*vulnmodel.VulnerabilityAttachment{
			11: attachment(11, 7, vulnmodel.AttachmentStatusReady, uint64Ptr(99)),
		},
	}
	svc := newAttachmentService(repo)

	err := svc.ValidateDescriptionAttachments(context.Background(), 7, 0, []uint64{11})

	require.ErrorIs(t, err, ErrInvalidAttachmentReference)
	require.Empty(t, repo.attachedIDs)
}
