package storage

import (
	"context"
	"errors"
	"reflect"
	"strings"
	"testing"
	"time"

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

// ---- mock DBTX for workspace slot queries ----

type slotFakeResult struct {
	row db.WorkspaceSlot
	err error
}

type slotQueryCall struct {
	sql  string
	args []interface{}
}

type slotMockDBTX struct {
	// result drives every QueryRow (:one) call.
	result slotFakeResult
	// listRows / listErr drive the Query (:many) call used by list.
	listRows []db.WorkspaceSlot
	listErr  error

	queryLog   []string
	queryCalls []slotQueryCall
}

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

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

func (m *slotMockDBTX) QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row {
	m.queryLog = append(m.queryLog, sql)
	m.queryCalls = append(m.queryCalls, slotQueryCall{sql: sql, args: args})
	return &slotMockRow{result: m.result}
}

type slotMockRow struct {
	result slotFakeResult
}

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

// fakeSlotRows is a minimal pgx.Rows over a fixed slice of workspace slots.
type fakeSlotRows struct {
	rows []db.WorkspaceSlot
	idx  int
	err  error
}

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

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

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

// scanWorkspaceSlot fills scan destinations in the column order used by the
// generated workspace_slots queries.
func scanWorkspaceSlot(row db.WorkspaceSlot, dest []interface{}) {
	if len(dest) < 7 {
		return
	}
	if p, ok := dest[0].(*int64); ok {
		*p = row.ID
	}
	if p, ok := dest[1].(*int64); ok {
		*p = row.ProjectSyncSettingID
	}
	if p, ok := dest[2].(*int32); ok {
		*p = row.SlotIndex
	}
	if p, ok := dest[3].(*string); ok {
		*p = row.State
	}
	if p, ok := dest[4].(*string); ok {
		*p = row.Path
	}
	if p, ok := dest[5].(*time.Time); ok {
		*p = row.CreatedAt
	}
	if p, ok := dest[6].(*time.Time); ok {
		*p = row.UpdatedAt
	}
}

func slotSQLContains(logs []string, sub string) bool {
	for _, l := range logs {
		if strings.Contains(l, sub) {
			return true
		}
	}
	return false
}

func newSlotStore(m *slotMockDBTX) *Store {
	return NewStoreWithQueries(db.New(m))
}

// ---- ReserveWorkspaceSlot ----

func TestReserveWorkspaceSlot_ReservesAvailableAndReturnsRow(t *testing.T) {
	m := &slotMockDBTX{result: slotFakeResult{row: db.WorkspaceSlot{
		ID: 11, ProjectSyncSettingID: 5, SlotIndex: 2, State: "in_use", Path: "/ws/nomadcode/002",
	}}}
	store := newSlotStore(m)

	slot, err := store.ReserveWorkspaceSlot(context.Background(), 5)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if slot.ID != 11 || slot.ProjectSyncSettingID != 5 || slot.SlotIndex != 2 {
		t.Errorf("identity not preserved: %+v", slot)
	}
	if slot.State != "in_use" {
		t.Errorf("state: got %q, want in_use", slot.State)
	}
	if slot.Path != "/ws/nomadcode/002" {
		t.Errorf("path: got %q", slot.Path)
	}
}

func TestReserveWorkspaceSlot_AtomicGuardsInQuery(t *testing.T) {
	m := &slotMockDBTX{result: slotFakeResult{row: db.WorkspaceSlot{ID: 1, State: "in_use"}}}
	store := newSlotStore(m)

	if _, err := store.ReserveWorkspaceSlot(context.Background(), 7); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	for _, fragment := range []string{
		"state = 'available'",
		"SET state = 'in_use'",
		"FOR UPDATE SKIP LOCKED",
		"LIMIT 1",
	} {
		if !slotSQLContains(m.queryLog, fragment) {
			t.Errorf("expected reserve query to contain %q, queryLog=%v", fragment, m.queryLog)
		}
	}
	if len(m.queryCalls) == 0 {
		t.Fatal("expected a query call")
	}
	if got := m.queryCalls[0].args; len(got) != 1 || !reflect.DeepEqual(got[0], int64(7)) {
		t.Errorf("reserve args: got %v, want [int64(7)]", got)
	}
}

func TestReserveWorkspaceSlot_NoAvailableReturnsSentinel(t *testing.T) {
	m := &slotMockDBTX{result: slotFakeResult{err: pgx.ErrNoRows}}
	store := newSlotStore(m)

	_, err := store.ReserveWorkspaceSlot(context.Background(), 5)
	if !errors.Is(err, ErrNoAvailableWorkspaceSlot) {
		t.Fatalf("expected ErrNoAvailableWorkspaceSlot, got %v", err)
	}
}

func TestReserveWorkspaceSlot_PreservesGenericDBError(t *testing.T) {
	sentinel := errors.New("connection reset")
	m := &slotMockDBTX{result: slotFakeResult{err: sentinel}}
	store := newSlotStore(m)

	_, err := store.ReserveWorkspaceSlot(context.Background(), 5)
	if !errors.Is(err, sentinel) {
		t.Fatalf("expected sentinel DB error, got %v", err)
	}
	if errors.Is(err, ErrNoAvailableWorkspaceSlot) {
		t.Error("generic DB error should not be reported as no-available")
	}
}

// ---- UpdateWorkspaceSlotState ----

func TestUpdateWorkspaceSlotState_PassesArgsAndGuard(t *testing.T) {
	m := &slotMockDBTX{result: slotFakeResult{row: db.WorkspaceSlot{ID: 9, State: "dirty"}}}
	store := newSlotStore(m)

	slot, err := store.UpdateWorkspaceSlotState(context.Background(), db.UpdateWorkspaceSlotStateParams{
		ID: 9, State: "dirty",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if slot.State != "dirty" {
		t.Errorf("state: got %q", slot.State)
	}
	if !slotSQLContains(m.queryLog, "SET state = $2") {
		t.Errorf("expected parameterized state update, queryLog=%v", m.queryLog)
	}
	if got := m.queryCalls[0].args; len(got) != 2 ||
		!reflect.DeepEqual(got[0], int64(9)) ||
		!reflect.DeepEqual(got[1], "dirty") {
		t.Errorf("update args: got %v, want [int64(9) dirty]", got)
	}
}

// ---- UpsertWorkspaceSlot ----

func TestUpsertWorkspaceSlot_PassesArgsAndKeepsState(t *testing.T) {
	m := &slotMockDBTX{result: slotFakeResult{row: db.WorkspaceSlot{
		ID: 3, ProjectSyncSettingID: 5, SlotIndex: 0, State: "available", Path: "/ws/nomadcode/000",
	}}}
	store := newSlotStore(m)

	slot, err := store.UpsertWorkspaceSlot(context.Background(), db.UpsertWorkspaceSlotParams{
		ProjectSyncSettingID: 5, SlotIndex: 0, Path: "/ws/nomadcode/000",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if slot.State != "available" {
		t.Errorf("upserted state: got %q", slot.State)
	}
	if !slotSQLContains(m.queryLog, "INSERT INTO workspace_slots") ||
		!slotSQLContains(m.queryLog, "ON CONFLICT (project_sync_setting_id, slot_index)") {
		t.Errorf("expected upsert query shape, queryLog=%v", m.queryLog)
	}
	if got := m.queryCalls[0].args; len(got) != 3 ||
		!reflect.DeepEqual(got[0], int64(5)) ||
		!reflect.DeepEqual(got[1], int32(0)) ||
		!reflect.DeepEqual(got[2], "/ws/nomadcode/000") {
		t.Errorf("upsert args: got %v, want [int64(5) int32(0) path]", got)
	}
}

// ---- ListWorkspaceSlots ----

func TestListWorkspaceSlots_ReturnsRowsForEachState(t *testing.T) {
	m := &slotMockDBTX{listRows: []db.WorkspaceSlot{
		{ID: 1, ProjectSyncSettingID: 5, SlotIndex: 0, State: "available", Path: "/ws/000"},
		{ID: 2, ProjectSyncSettingID: 5, SlotIndex: 1, State: "in_use", Path: "/ws/001"},
		{ID: 3, ProjectSyncSettingID: 5, SlotIndex: 2, State: "dirty", Path: "/ws/002"},
		{ID: 4, ProjectSyncSettingID: 5, SlotIndex: 3, State: "error", Path: "/ws/003"},
	}}
	store := newSlotStore(m)

	slots, err := store.ListWorkspaceSlots(context.Background(), 5)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	want := []string{"available", "in_use", "dirty", "error"}
	if len(slots) != len(want) {
		t.Fatalf("expected %d slots, got %d", len(want), len(slots))
	}
	for i, slot := range slots {
		if int(slot.SlotIndex) != i {
			t.Errorf("slot[%d] index: got %d", i, slot.SlotIndex)
		}
		if slot.State != want[i] {
			t.Errorf("slot[%d] state: got %q, want %q", i, slot.State, want[i])
		}
	}
	if got := m.queryCalls[0].args; len(got) != 1 || !reflect.DeepEqual(got[0], int64(5)) {
		t.Errorf("list args: got %v, want [int64(5)]", got)
	}
}

func TestListWorkspaceSlots_PropagatesQueryError(t *testing.T) {
	sentinel := errors.New("query failed")
	m := &slotMockDBTX{listErr: sentinel}
	store := newSlotStore(m)

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