package runners_test

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

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/jobs/runners"
)

type fakeAttachmentRepo struct {
	softDel        []runners.AttachmentRow
	orphan         []runners.AttachmentRow
	softDelListErr error
	orphanListErr  error
	deleteErr      error
	deletedIDs     []uuid.UUID
	softDelCalled  bool
	orphanCalled   bool
}

func (f *fakeAttachmentRepo) ListAttachmentsForSoftDeletedConversations(_ context.Context, _ time.Duration) ([]runners.AttachmentRow, error) {
	f.softDelCalled = true
	return f.softDel, f.softDelListErr
}
func (f *fakeAttachmentRepo) ListExpiredOrphans(_ context.Context, _ time.Duration) ([]runners.AttachmentRow, error) {
	f.orphanCalled = true
	return f.orphan, f.orphanListErr
}
func (f *fakeAttachmentRepo) DeleteAttachments(_ context.Context, ids []uuid.UUID) error {
	if f.deleteErr != nil {
		return f.deleteErr
	}
	f.deletedIDs = append(f.deletedIDs, ids...)
	return nil
}

type fakeStorage struct {
	deleted   []string
	err       error
	failPaths map[string]error
}

func (s *fakeStorage) Delete(_ context.Context, path string) error {
	if s.err != nil {
		return s.err
	}
	if e, ok := s.failPaths[path]; ok {
		return e
	}
	s.deleted = append(s.deleted, path)
	return nil
}

func TestAttachmentCleanup_DeletesBothCategories(t *testing.T) {
	t.Parallel()
	a, b, c := uuid.New(), uuid.New(), uuid.New()
	repo := &fakeAttachmentRepo{
		softDel: []runners.AttachmentRow{{ID: a, StoragePath: "a"}, {ID: b, StoragePath: "b"}},
		orphan:  []runners.AttachmentRow{{ID: c, StoragePath: "c"}},
	}
	st := &fakeStorage{}
	r := runners.NewAttachmentCleanup(repo, st, runners.AttachmentCleanupConfig{Retention: 30 * 24 * time.Hour}, discardLogger())
	r.Run(context.Background())
	assert.ElementsMatch(t, []string{"a", "b", "c"}, st.deleted)
	assert.ElementsMatch(t, []uuid.UUID{a, b, c}, repo.deletedIDs)
}

func TestAttachmentCleanup_StorageErrorDoesNotDeleteFromDB(t *testing.T) {
	t.Parallel()
	repo := &fakeAttachmentRepo{
		softDel: []runners.AttachmentRow{
			{ID: uuid.New(), StoragePath: "fail"},
			{ID: uuid.New(), StoragePath: "ok"},
		},
	}
	st := &fakeStorage{err: errors.New("disk full")}
	r := runners.NewAttachmentCleanup(repo, st, runners.AttachmentCleanupConfig{Retention: 30 * 24 * time.Hour}, discardLogger())
	r.Run(context.Background())
	require.Empty(t, st.deleted, "storage error → no path recorded")
	require.Empty(t, repo.deletedIDs, "storage error → no DB delete batch")
}

func TestAttachmentCleanup_PartialStorageSuccessOnlyDeletesSuccessfulIDs(t *testing.T) {
	t.Parallel()
	succID := uuid.New()
	failID := uuid.New()
	repo := &fakeAttachmentRepo{
		softDel: []runners.AttachmentRow{
			{ID: succID, StoragePath: "ok"},
			{ID: failID, StoragePath: "bad"},
		},
	}
	st := &fakeStorage{failPaths: map[string]error{"bad": errors.New("permission denied")}}
	r := runners.NewAttachmentCleanup(repo, st, runners.AttachmentCleanupConfig{Retention: 30 * 24 * time.Hour}, discardLogger())
	r.Run(context.Background())
	assert.ElementsMatch(t, []string{"ok"}, st.deleted, "only successful storage delete recorded")
	assert.ElementsMatch(t, []uuid.UUID{succID}, repo.deletedIDs, "DB delete batch only contains successfully-deleted-from-storage IDs")
}

func TestAttachmentCleanup_SoftDelListErrorDoesNotPreventOrphanPhase(t *testing.T) {
	t.Parallel()
	orphanID := uuid.New()
	repo := &fakeAttachmentRepo{
		softDelListErr: errors.New("db blip"),
		orphan:         []runners.AttachmentRow{{ID: orphanID, StoragePath: "p"}},
	}
	st := &fakeStorage{}
	r := runners.NewAttachmentCleanup(repo, st, runners.AttachmentCleanupConfig{Retention: 30 * 24 * time.Hour}, discardLogger())
	r.Run(context.Background())
	assert.True(t, repo.softDelCalled, "soft-del list was attempted")
	assert.True(t, repo.orphanCalled, "orphan list still ran after soft-del list error")
	assert.ElementsMatch(t, []string{"p"}, st.deleted, "orphan storage delete proceeded")
	assert.ElementsMatch(t, []uuid.UUID{orphanID}, repo.deletedIDs)
}

func TestAttachmentCleanup_DBDeleteErrorIsLoggedNotPanicked(t *testing.T) {
	t.Parallel()
	id := uuid.New()
	repo := &fakeAttachmentRepo{
		softDel:   []runners.AttachmentRow{{ID: id, StoragePath: "p"}},
		deleteErr: errors.New("constraint violation"),
	}
	st := &fakeStorage{}
	r := runners.NewAttachmentCleanup(repo, st, runners.AttachmentCleanupConfig{Retention: 30 * 24 * time.Hour}, discardLogger())
	require.NotPanics(t, func() {
		r.Run(context.Background())
	}, "DB delete error must not panic")
	assert.ElementsMatch(t, []string{"p"}, st.deleted, "storage delete proceeded before DB error")
	assert.Empty(t, repo.deletedIDs, "deleteErr means no IDs accumulated in fake")
}
