package taskboard

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/coredb/testkit"
)

func TestMarkTaskReadSerializesWithParentTaskDelete(t *testing.T) {
	t.Run("mark read locks parent then child before delete", func(t *testing.T) {
		ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
		defer cancel()
		fixture := seedTaskboardUserFixtureOnDB(t, ctx, startTaskReadLifecycleTiDB(t, ctx))
		parent, children := createTaskboardParentWithSubtasks(t, ctx, fixture)
		child := children[0]

		markTx, err := fixture.pool.BeginTx(ctx, coredb.TxOptions{})
		require.NoError(t, err)
		defer markTx.Rollback(ctx)

		_, err = fixture.service.InTx(markTx).MarkTaskRead(ctx, MarkTaskReadRequest{
			Context: taskboardUserContext(fixture),
			Task:    TaskRef{TaskID: child.ID},
		})
		require.NoError(t, err)

		deleteDone := make(chan error, 1)
		go func() {
			_, deleteErr := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
				Context:     taskboardUserContext(fixture),
				Task:        TaskRef{TaskID: parent.ID},
				SubtaskMode: DeleteTaskSubtaskModeDelete,
			})
			deleteDone <- deleteErr
		}()

		requireTaskLifecycleOperationBlocked(t, deleteDone, "parent delete overtook an uncommitted child mark-read")
		require.NoError(t, markTx.Commit(ctx))
		require.NoError(t, requireTaskLifecycleOperationDone(t, deleteDone))
		require.Equal(t, int64(0), countTaskRowsByID(t, ctx, fixture, parent.ID))
		require.Equal(t, int64(0), countTaskRowsByID(t, ctx, fixture, child.ID))
	})

	t.Run("delete commit suppresses waiting mark read", func(t *testing.T) {
		ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
		defer cancel()
		fixture := seedTaskboardUserFixtureOnDB(t, ctx, startTaskReadLifecycleTiDB(t, ctx))
		parent, children := createTaskboardParentWithSubtasks(t, ctx, fixture)
		child := children[0]

		deleteTx, err := fixture.pool.BeginTx(ctx, coredb.TxOptions{})
		require.NoError(t, err)
		defer deleteTx.Rollback(ctx)

		_, err = fixture.service.InTx(deleteTx).DeleteTask(ctx, DeleteTaskRequest{
			Context:     taskboardUserContext(fixture),
			Task:        TaskRef{TaskID: parent.ID},
			SubtaskMode: DeleteTaskSubtaskModeDelete,
		})
		require.NoError(t, err)

		markDone := make(chan error, 1)
		go func() {
			_, markErr := fixture.service.MarkTaskRead(ctx, MarkTaskReadRequest{
				Context: taskboardUserContext(fixture),
				Task:    TaskRef{TaskID: child.ID},
			})
			markDone <- markErr
		}()

		requireTaskLifecycleOperationBlocked(t, markDone, "child mark-read passed an uncommitted parent delete")
		require.NoError(t, deleteTx.Commit(ctx))
		require.ErrorIs(t, requireTaskLifecycleOperationDone(t, markDone), ErrNotFound)
		require.Equal(t, int64(0), countTaskRowsByID(t, ctx, fixture, parent.ID))
		require.Equal(t, int64(0), countTaskRowsByID(t, ctx, fixture, child.ID))
	})
}

func startTaskReadLifecycleTiDB(t *testing.T, ctx context.Context) coredb.DB {
	t.Helper()
	dsn := testkit.StartInitializedTiDBWithRootDSN(t, taskboardTiDB.RootDSN(t))
	sqlDB, err := testkit.OpenTiDBContext(ctx, dsn)
	require.NoError(t, err)
	sqlDB.SetMaxOpenConns(4)
	sqlDB.SetMaxIdleConns(4)
	t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
	return coredb.WrapSQLDB(sqlDB)
}

func requireTaskLifecycleOperationBlocked(t *testing.T, done <-chan error, message string) {
	t.Helper()
	select {
	case err := <-done:
		t.Fatalf("%s: completed early with %v", message, err)
	case <-time.After(250 * time.Millisecond):
	}
}

func requireTaskLifecycleOperationDone(t *testing.T, done <-chan error) error {
	t.Helper()
	select {
	case err := <-done:
		return err
	case <-time.After(10 * time.Second):
		t.Fatal("timed out waiting for Task lifecycle operation")
		return nil
	}
}
