package migration

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

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

func TestExactMigrationJobLeaseCursorAndMappingAreResumable(t *testing.T) {
	ctx := context.Background()
	db := openMigrationTestDB(t)
	store := New(db)
	now := time.Date(2026, 8, 21, 4, 0, 0, 0, time.UTC)
	params := AcquireParams{
		ID: "cmj_1", WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_1", SourceCursor: "head",
		RepositoryID: "42", CommitSHA: strings.Repeat("a", 40), LeaseOwner: "worker_1",
		LeaseUntil: now.Add(3 * time.Minute), Now: now,
	}
	job, err := store.AcquireExactJob(ctx, params)
	require.NoError(t, err)
	require.Equal(t, MigrationCursorPending, job.Cursor)

	competing := params
	competing.ID = "cmj_competing"
	competing.LeaseOwner = "worker_2"
	_, err = store.AcquireExactJob(ctx, competing)
	require.ErrorIs(t, err, ErrMigrationLeaseHeld)

	job, err = store.CheckpointPublished(ctx, CheckpointPublishedParams{
		JobID: job.ID, LeaseOwner: params.LeaseOwner, ContentID: "ctn_1", ContentVersionID: "ctv_1",
		RootDigest: strings.Repeat("b", 64), FileCount: 2, SizeBytes: 12, Now: now.Add(time.Second),
	})
	require.NoError(t, err)
	require.Equal(t, MigrationCursorPublished, job.Cursor)
	job, err = store.VerifyMapping(ctx, VerifyMappingParams{MapID: "cmm_1", JobID: job.ID, LeaseOwner: params.LeaseOwner, Now: now.Add(2 * time.Second)})
	require.NoError(t, err)
	require.Equal(t, MigrationCursorMapped, job.Cursor)
	job, err = store.CompleteJob(ctx, job.ID, params.LeaseOwner, now.Add(3*time.Second))
	require.NoError(t, err)
	require.Equal(t, MigrationCursorCompleted, job.Cursor)
	require.Empty(t, job.LeaseOwner)

	mapping, err := store.GetMapping(ctx, params.RepositoryID, params.CommitSHA)
	require.NoError(t, err)
	require.Equal(t, "ctv_1", mapping.ContentVersionID)
	require.NotNil(t, mapping.VerifiedAt)

	replayed, err := store.AcquireExactJob(ctx, AcquireParams{
		ID: "cmj_replay", WorkspaceID: params.WorkspaceID, Domain: params.Domain, BindingKey: params.BindingKey,
		SourceCursor: params.SourceCursor, RepositoryID: params.RepositoryID, CommitSHA: params.CommitSHA,
		LeaseOwner: "worker_2", LeaseUntil: now.Add(4 * time.Minute), Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, job.ID, replayed.ID)
	require.Equal(t, MigrationCursorCompleted, replayed.Cursor)
	require.Empty(t, replayed.LeaseOwner)
}

func TestExactMigrationJobRejectsSourceIdentityChange(t *testing.T) {
	ctx := context.Background()
	db := openMigrationTestDB(t)
	store := New(db)
	now := time.Now().UTC()
	params := AcquireParams{
		ID: "cmj_1", WorkspaceID: "wsp_1", Domain: "role", BindingKey: "role_1", SourceCursor: "rlv_1",
		RepositoryID: "42", CommitSHA: strings.Repeat("a", 40), LeaseOwner: "worker_1",
		LeaseUntil: now.Add(time.Minute), Now: now,
	}
	_, err := store.AcquireExactJob(ctx, params)
	require.NoError(t, err)
	require.NoError(t, store.ReleaseFailure(ctx, "cmj_1", "worker_1", context.Canceled, now.Add(time.Second)))
	params.ID = "cmj_2"
	params.CommitSHA = strings.Repeat("b", 40)
	params.Now = now.Add(2 * time.Second)
	params.LeaseUntil = params.Now.Add(time.Minute)
	_, err = store.AcquireExactJob(ctx, params)
	require.ErrorIs(t, err, ErrMigrationConflict)
}

func TestExactMigrationJobExpiredLeaseFencesOldOwner(t *testing.T) {
	ctx := context.Background()
	store := New(openMigrationTestDB(t))
	now := time.Date(2026, 8, 21, 6, 0, 0, 0, time.UTC)
	params := AcquireParams{
		ID: "cmj_1", WorkspaceID: "wsp_1", Domain: "skill", BindingKey: "skill_1", SourceCursor: "head",
		RepositoryID: "42", CommitSHA: strings.Repeat("a", 40), LeaseOwner: "lease_old",
		LeaseUntil: now.Add(time.Minute), Now: now,
	}
	job, err := store.AcquireExactJob(ctx, params)
	require.NoError(t, err)

	params.ID = "cmj_unused"
	params.LeaseOwner = "lease_new"
	params.Now = now.Add(2 * time.Minute)
	params.LeaseUntil = params.Now.Add(time.Minute)
	job, err = store.AcquireExactJob(ctx, params)
	require.NoError(t, err)
	require.Equal(t, "lease_new", job.LeaseOwner)

	_, err = store.CheckpointPublished(ctx, CheckpointPublishedParams{
		JobID: job.ID, LeaseOwner: "lease_old", ContentID: "ctn_old", ContentVersionID: "ctv_old",
		RootDigest: strings.Repeat("b", 64), FileCount: 1, SizeBytes: 1, Now: params.Now.Add(time.Second),
	})
	require.ErrorIs(t, err, ErrMigrationLeaseHeld)
	require.ErrorIs(t, store.ReleaseFailure(ctx, job.ID, "lease_old", context.Canceled, params.Now.Add(time.Second)), ErrMigrationLeaseHeld)

	current, err := store.GetJob(ctx, params.WorkspaceID, params.Domain, params.BindingKey, params.SourceCursor)
	require.NoError(t, err)
	require.Equal(t, "lease_new", current.LeaseOwner)
	require.Equal(t, MigrationCursorPending, current.Cursor)
}

func TestExactMigrationMappingConflictFailsClosed(t *testing.T) {
	ctx := context.Background()
	store := New(openMigrationTestDB(t))
	now := time.Date(2026, 8, 21, 7, 0, 0, 0, time.UTC)
	createMapped := func(id, binding, contentID, versionID, leaseOwner string) error {
		job, err := store.AcquireExactJob(ctx, AcquireParams{
			ID: id, WorkspaceID: "wsp_1", Domain: "role", BindingKey: binding, SourceCursor: id,
			RepositoryID: "42", CommitSHA: strings.Repeat("c", 40), LeaseOwner: leaseOwner,
			LeaseUntil: now.Add(3 * time.Minute), Now: now,
		})
		if err != nil {
			return err
		}
		job, err = store.CheckpointPublished(ctx, CheckpointPublishedParams{
			JobID: job.ID, LeaseOwner: leaseOwner, ContentID: contentID, ContentVersionID: versionID,
			RootDigest: strings.Repeat("d", 64), FileCount: 1, SizeBytes: 1, Now: now.Add(time.Second),
		})
		if err != nil {
			return err
		}
		_, err = store.VerifyMapping(ctx, VerifyMappingParams{MapID: "map_" + id, JobID: job.ID, LeaseOwner: leaseOwner, Now: now.Add(2 * time.Second)})
		return err
	}
	require.NoError(t, createMapped("cmj_1", "role_1", "ctn_1", "ctv_1", "lease_1"))
	require.ErrorIs(t, createMapped("cmj_2", "role_2", "ctn_2", "ctv_2", "lease_2"), ErrMigrationConflict)
}

func TestExactMigrationJobAttemptLimitStopsFurtherClaims(t *testing.T) {
	ctx := context.Background()
	store := New(openMigrationTestDB(t))
	base := time.Date(2026, 8, 21, 8, 0, 0, 0, time.UTC)
	params := AcquireParams{
		WorkspaceID: "wsp_1", Domain: "role", BindingKey: "role_1", SourceCursor: "rlv_1",
		RepositoryID: "42", CommitSHA: strings.Repeat("e", 40),
	}
	for attempt := 0; attempt < MaxMigrationAttempts; attempt++ {
		params.ID = fmt.Sprintf("cmj_%d", attempt)
		params.LeaseOwner = fmt.Sprintf("lease_%d", attempt)
		params.Now = base.Add(time.Duration(attempt) * time.Minute)
		params.LeaseUntil = params.Now.Add(30 * time.Second)
		job, err := store.AcquireExactJob(ctx, params)
		require.NoError(t, err)
		require.Equal(t, attempt+1, job.Attempts)
		require.NoError(t, store.ReleaseFailure(ctx, job.ID, params.LeaseOwner, context.Canceled, params.Now.Add(time.Second)))
	}
	params.ID = "cmj_rejected"
	params.LeaseOwner = "lease_rejected"
	params.Now = base.Add(MaxMigrationAttempts * time.Minute)
	params.LeaseUntil = params.Now.Add(30 * time.Second)
	_, err := store.AcquireExactJob(ctx, params)
	require.ErrorIs(t, err, ErrMigrationConflict)
}

func openMigrationTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&Mapping{}, &Job{}))
	sqlDB, err := db.DB()
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
	return db
}
