package storage

import (
	"context"
	"encoding/json"
	"strings"
	"testing"
	"time"

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

// ---- mock DBTX for task queries ----

type taskFakeResult struct {
	row db.Task
	err error
}

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

type taskMockDBTX struct {
	result     taskFakeResult
	queryCalls []taskQueryCall
}

func (m *taskMockDBTX) Exec(_ context.Context, _ string, _ ...interface{}) (pgconn.CommandTag, error) {
	return pgconn.NewCommandTag("OK"), nil
}

func (m *taskMockDBTX) Query(_ context.Context, _ string, _ ...interface{}) (pgx.Rows, error) {
	return nil, nil
}

func (m *taskMockDBTX) QueryRow(_ context.Context, sql string, args ...interface{}) pgx.Row {
	m.queryCalls = append(m.queryCalls, taskQueryCall{sql: sql, args: args})
	return &taskMockRow{result: m.result}
}

type taskMockRow struct {
	result taskFakeResult
}

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

// scanTask fills scan destinations in the column order returned by task queries.
func scanTask(row db.Task, dest []interface{}) {
	if len(dest) < 14 {
		return
	}
	if p, ok := dest[0].(*string); ok {
		*p = row.ID
	}
	if p, ok := dest[1].(*string); ok {
		*p = row.Title
	}
	if p, ok := dest[2].(*string); ok {
		*p = row.Source
	}
	if p, ok := dest[3].(*string); ok {
		*p = row.Status
	}
	if p, ok := dest[4].(*json.RawMessage); ok {
		*p = row.Payload
	}
	if p, ok := dest[5].(*json.RawMessage); ok {
		*p = row.Result
	}
	if p, ok := dest[6].(**string); ok {
		*p = row.Error
	}
	if p, ok := dest[7].(*time.Time); ok {
		*p = row.CreatedAt
	}
	if p, ok := dest[8].(*time.Time); ok {
		*p = row.UpdatedAt
	}
	if p, ok := dest[9].(**string); ok {
		*p = row.ExternalProvider
	}
	if p, ok := dest[10].(**string); ok {
		*p = row.ExternalID
	}
	if p, ok := dest[11].(**string); ok {
		*p = row.ExternalUrl
	}
	if p, ok := dest[12].(*json.RawMessage); ok {
		*p = row.ExternalMetadata
	}
	if p, ok := dest[13].(*json.RawMessage); ok {
		*p = row.Metadata
	}
}

func taskSQLContains(calls []taskQueryCall, sub string) bool {
	for _, c := range calls {
		if strings.Contains(c.sql, sub) {
			return true
		}
	}
	return false
}

func newTaskStore(m *taskMockDBTX) *Store {
	return NewStoreWithQueries(db.New(m))
}

func strPtr(s string) *string { return &s }

// ---- CreateTask ----

func TestCreateTask_UsesExternalRefUpsert(t *testing.T) {
	provider := "plane"
	id := "work-1"
	m := &taskMockDBTX{result: taskFakeResult{row: db.Task{
		ID:               "task-abc",
		Title:            "Fix issue",
		Source:           "plane",
		Status:           "pending",
		Payload:          json.RawMessage(`{}`),
		ExternalProvider: strPtr(provider),
		ExternalID:       strPtr(id),
		ExternalMetadata: json.RawMessage(`{}`),
		Metadata:         json.RawMessage(`{}`),
	}}}
	store := newTaskStore(m)

	task, err := store.CreateTask(context.Background(), CreateTaskInput{
		Title:            "Fix issue",
		Source:           "plane",
		Payload:          json.RawMessage(`{}`),
		ExternalProvider: strPtr(provider),
		ExternalID:       strPtr(id),
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if task.ID != "task-abc" {
		t.Errorf("task ID: got %q, want task-abc", task.ID)
	}

	if len(m.queryCalls) == 0 {
		t.Fatal("expected a query call")
	}
	sql := m.queryCalls[0].sql
	for _, fragment := range []string{
		"ON CONFLICT (external_provider, external_id)",
		"WHERE external_provider IS NOT NULL AND external_id IS NOT NULL",
		"DO UPDATE SET updated_at",
	} {
		if !strings.Contains(sql, fragment) {
			t.Errorf("expected CreateTask SQL to contain %q\ngot: %s", fragment, sql)
		}
	}

	args := m.queryCalls[0].args
	if len(args) < 8 {
		t.Fatalf("expected at least 8 args, got %d", len(args))
	}
	// args order: title, source, payload, metadata, external_provider, external_id, external_url, external_metadata
	if got, ok := args[4].(*string); !ok || *got != provider {
		t.Errorf("arg[4] external_provider: got %v, want %q", args[4], provider)
	}
	if got, ok := args[5].(*string); !ok || *got != id {
		t.Errorf("arg[5] external_id: got %v, want %q", args[5], id)
	}
}

// ---- GetTaskByExternalRef ----

func TestGetTaskByExternalRef_PassesProviderAndID(t *testing.T) {
	provider := "plane"
	id := "work-1"
	m := &taskMockDBTX{result: taskFakeResult{row: db.Task{
		ID:               "task-xyz",
		Title:            "Existing task",
		Source:           "plane",
		Status:           "pending",
		Payload:          json.RawMessage(`{}`),
		ExternalProvider: strPtr(provider),
		ExternalID:       strPtr(id),
		ExternalMetadata: json.RawMessage(`{}`),
		Metadata:         json.RawMessage(`{}`),
	}}}
	store := newTaskStore(m)

	task, err := store.GetTaskByExternalRef(context.Background(), provider, id)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if task.ID != "task-xyz" {
		t.Errorf("task ID: got %q, want task-xyz", task.ID)
	}

	if len(m.queryCalls) == 0 {
		t.Fatal("expected a query call")
	}
	sql := m.queryCalls[0].sql
	if !strings.Contains(sql, "WHERE external_provider = $1 AND external_id = $2") {
		t.Errorf("expected WHERE clause with provider and id params\ngot: %s", sql)
	}

	args := m.queryCalls[0].args
	if len(args) < 2 {
		t.Fatalf("expected 2 args, got %d", len(args))
	}
	if got, ok := args[0].(*string); !ok || *got != provider {
		t.Errorf("arg[0] external_provider: got %v, want %q", args[0], provider)
	}
	if got, ok := args[1].(*string); !ok || *got != id {
		t.Errorf("arg[1] external_id: got %v, want %q", args[1], id)
	}
}

func TestGetTaskByExternalRef_PropagatesNoRows(t *testing.T) {
	m := &taskMockDBTX{result: taskFakeResult{err: pgx.ErrNoRows}}
	store := newTaskStore(m)

	_, err := store.GetTaskByExternalRef(context.Background(), "plane", "missing")
	if err != pgx.ErrNoRows {
		t.Fatalf("expected pgx.ErrNoRows, got %v", err)
	}
}
