package storage

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

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/nomadcode/nomadcode-core/internal/db"
)

// stepMockDBTX answers the ledger list (Query) and mark (QueryRow) calls.
type stepMockDBTX struct {
	listRows []db.RoadmapSyncStep
	listErr  error
	markRow  db.RoadmapSyncStep
	markErr  error

	queryLog []string
	markArgs []interface{}
}

func (m *stepMockDBTX) Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) {
	return pgconn.NewCommandTag("OK"), nil
}

func (m *stepMockDBTX) Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) {
	m.queryLog = append(m.queryLog, sql)
	if m.listErr != nil {
		return nil, m.listErr
	}
	return &fakeStepRows{rows: m.listRows}, nil
}

func (m *stepMockDBTX) QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row {
	m.queryLog = append(m.queryLog, sql)
	m.markArgs = args
	return &stepMockRow{row: m.markRow, err: m.markErr}
}

type stepMockRow struct {
	row db.RoadmapSyncStep
	err error
}

func (r *stepMockRow) Scan(dest ...interface{}) error {
	if r.err != nil {
		return r.err
	}
	scanRoadmapSyncStep(r.row, dest)
	return nil
}

type fakeStepRows struct {
	rows []db.RoadmapSyncStep
	idx  int
}

func (r *fakeStepRows) Close()                                       {}
func (r *fakeStepRows) Err() error                                   { return nil }
func (r *fakeStepRows) CommandTag() pgconn.CommandTag                { return pgconn.NewCommandTag("SELECT") }
func (r *fakeStepRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
func (r *fakeStepRows) Values() ([]interface{}, error)               { return nil, nil }
func (r *fakeStepRows) RawValues() [][]byte                          { return nil }
func (r *fakeStepRows) Conn() *pgx.Conn                              { return nil }

func (r *fakeStepRows) Next() bool {
	if r.idx >= len(r.rows) {
		return false
	}
	r.idx++
	return true
}

func (r *fakeStepRows) Scan(dest ...interface{}) error {
	scanRoadmapSyncStep(r.rows[r.idx-1], dest)
	return nil
}

func scanRoadmapSyncStep(row db.RoadmapSyncStep, dest []interface{}) {
	if len(dest) < 4 {
		return
	}
	assign(dest[0], row.ID)
	assign(dest[1], row.RoadmapSyncIdentityID)
	assign(dest[2], row.Step)
	if p, ok := dest[3].(*time.Time); ok {
		*p = row.CompletedAt
	}
}

func newStepStore(m *stepMockDBTX) *Store {
	return NewStoreWithQueries(db.New(m))
}

// ---- MarkStepCompleted ----

func TestMarkStepCompleted_KnownStepPersists(t *testing.T) {
	completedAt := time.Now()
	m := &stepMockDBTX{markRow: db.RoadmapSyncStep{
		ID: 1, RoadmapSyncIdentityID: 7, Step: string(StepDevelopMatched), CompletedAt: completedAt,
	}}
	store := newStepStore(m)

	row, err := store.MarkStepCompleted(context.Background(), 7, StepDevelopMatched)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if row.Step != string(StepDevelopMatched) || row.RoadmapSyncIdentityID != 7 {
		t.Errorf("row not preserved: %+v", row)
	}
	if !sqlLogContains(m.queryLog, "ON CONFLICT (roadmap_sync_identity_id, step)") {
		t.Errorf("expected idempotent upsert shape, log=%v", m.queryLog)
	}
}

func TestMarkStepCompleted_CompletedStepReapplyIsNoOp(t *testing.T) {
	// The DB upsert returns the original row (same completed_at) on re-mark; the
	// store passes it through without error so a re-apply is a no-op.
	original := time.Date(2026, 6, 13, 10, 0, 0, 0, time.UTC)
	m := &stepMockDBTX{markRow: db.RoadmapSyncStep{
		ID: 1, RoadmapSyncIdentityID: 7, Step: string(StepPlaneBodyUpdated), CompletedAt: original,
	}}
	store := newStepStore(m)

	row, err := store.MarkStepCompleted(context.Background(), 7, StepPlaneBodyUpdated)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !row.CompletedAt.Equal(original) {
		t.Errorf("re-apply must keep original completed_at, got %v", row.CompletedAt)
	}
}

func TestMarkStepCompleted_UnknownStepRejected(t *testing.T) {
	m := &stepMockDBTX{}
	store := newStepStore(m)

	_, err := store.MarkStepCompleted(context.Background(), 7, RoadmapSyncStep("plane_done_moved"))
	if !errors.Is(err, ErrUnknownRoadmapSyncStep) {
		t.Fatalf("expected unknown-step error, got %v", err)
	}
	if len(m.queryLog) != 0 {
		t.Errorf("unknown step must not touch the DB, log=%v", m.queryLog)
	}
}

// ---- CompletedSteps ----

func TestCompletedSteps_ReturnsSetForResume(t *testing.T) {
	m := &stepMockDBTX{listRows: []db.RoadmapSyncStep{
		{ID: 1, RoadmapSyncIdentityID: 7, Step: string(StepDevelopMatched)},
		{ID: 2, RoadmapSyncIdentityID: 7, Step: string(StepOriginalCommentPreserved)},
	}}
	store := newStepStore(m)

	done, err := store.CompletedSteps(context.Background(), 7)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !done[StepDevelopMatched] || !done[StepOriginalCommentPreserved] {
		t.Errorf("expected first two steps completed, got %v", done)
	}
	if done[StepPlaneBodyUpdated] || done[StepPlaneTodoMoved] {
		t.Errorf("uncompleted steps must be absent, got %v", done)
	}
}

func TestCompletedSteps_PropagatesQueryError(t *testing.T) {
	sentinel := errors.New("query failed")
	m := &stepMockDBTX{listErr: sentinel}
	store := newStepStore(m)

	_, err := store.CompletedSteps(context.Background(), 7)
	if !errors.Is(err, sentinel) {
		t.Fatalf("expected sentinel error, got %v", err)
	}
}
