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"
)

// SQL template keys matching the const strings in db/project_sync_settings.sql.go
const (
	sqlCreateKey = `-- name: CreateProjectSyncSetting :one
INSERT INTO project_sync_settings (
    provider, tenant, project, git_remote_url, source_branch,
    workspace_id, workspace_base_path, repo_dir_name, active
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, TRUE)
RETURNING id, provider, tenant, project, git_remote_url, source_branch,
          workspace_id, workspace_base_path, repo_dir_name, active,
          created_at, updated_at
`
	sqlGetActiveKey = `-- name: GetActiveProjectSyncSettingByTarget :one
SELECT id, provider, tenant, project, git_remote_url, source_branch,
       workspace_id, workspace_base_path, repo_dir_name, active,
       created_at, updated_at
FROM project_sync_settings
WHERE provider = $1 AND tenant = $2 AND project = $3 AND active = TRUE
`
	sqlDeactKey = `-- name: DeactivateProjectSyncSetting :one
UPDATE project_sync_settings
SET active = FALSE, updated_at = now()
WHERE provider = $1 AND tenant = $2 AND project = $3 AND active = TRUE
RETURNING id, provider, tenant, project, git_remote_url, source_branch,
          workspace_id, workspace_base_path, repo_dir_name, active,
          created_at, updated_at
`
)

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

type mockDBTX struct {
	queries    map[string]fakeQueryResult
	queryLog   []string
	queryCalls []queryCall
}

type fakeQueryResult struct {
	row db.ProjectSyncSetting
	err error
}

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

func (m *mockDBTX) Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) {
	return nil, errors.New("implement me")
}

func (m *mockDBTX) QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row {
	m.queryLog = append(m.queryLog, sql)
	m.queryCalls = append(m.queryCalls, queryCall{sql: sql, args: args})
	if r, ok := m.queries[sql]; ok {
		return &mockRow{result: r}
	}
	return &mockRow{result: fakeQueryResult{err: pgx.ErrNoRows}}
}

type mockRow struct {
	result fakeQueryResult
}

func (f *mockRow) Scan(dest ...interface{}) error {
	if f.result.err != nil {
		return f.result.err
	}
	// Scan the saved row into dest pointers (simulates real pgx.Row.Scan).
	row := f.result.row
	dests := dest
	if len(dests) >= 12 {
		if p, ok := dests[0].(*int64); ok {
			*p = row.ID
		}
		if p, ok := dests[1].(*string); ok {
			*p = row.Provider
		}
		if p, ok := dests[2].(*string); ok {
			*p = row.Tenant
		}
		if p, ok := dests[3].(*string); ok {
			*p = row.Project
		}
		if p, ok := dests[4].(*string); ok {
			*p = row.GitRemoteUrl
		}
		if p, ok := dests[5].(*string); ok {
			*p = row.SourceBranch
		}
		if p, ok := dests[6].(*string); ok {
			*p = row.WorkspaceID
		}
		if p, ok := dests[7].(*string); ok {
			*p = row.WorkspaceBasePath
		}
		if p, ok := dests[8].(*string); ok {
			*p = row.RepoDirName
		}
		if p, ok := dests[9].(*bool); ok {
			*p = row.Active
		}
		if p, ok := dests[10].(*time.Time); ok {
			*p = row.CreatedAt
		}
		if p, ok := dests[11].(*time.Time); ok {
			*p = row.UpdatedAt
		}
	}
	return nil
}

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

// assertArgsAt asserts that the recorded args at the given index match expected values.
func assertArgsAt(t *testing.T, calls []queryCall, idx int, expected []interface{}, label string) {
	t.Helper()
	if idx >= len(calls) {
		t.Fatalf("[%s] expected call at index %d, but only %d calls recorded", label, idx, len(calls))
	}
	got := calls[idx].args
	if len(got) != len(expected) {
		t.Errorf("[%s] call[%d] args: expected %d args, got %d: %v", label, idx, len(expected), len(got), got)
		return
	}
	for i := range expected {
		if !reflect.DeepEqual(got[i], expected[i]) {
			t.Errorf("[%s] call[%d] args[%d]: expected %v (type %T), got %v (type %T)",
				label, idx, i, expected[i], expected[i], got[i], got[i])
		}
	}
}

func TestProjectSyncQueryCompile(t *testing.T) {
	_ = pgx.ErrNoRows
}

func TestCreateProjectSyncSettingParams(t *testing.T) {
	p := db.CreateProjectSyncSettingParams{
		Provider: "plane", Tenant: "general", Project: "project-1",
		GitRemoteUrl: "git@example.com:nomadcode.git", SourceBranch: "main",
		WorkspaceID: "ws-1", WorkspaceBasePath: "/home/user/workspace", RepoDirName: "nomadcode",
	}
	if p.Provider != "plane" {
		t.Errorf("Provider: got %q", p.Provider)
	}
	if p.GitRemoteUrl != "git@example.com:nomadcode.git" {
		t.Errorf("GitRemoteUrl: got %q", p.GitRemoteUrl)
	}
}

func TestGetActiveProjectSyncSettingByTargetParams(t *testing.T) {
	p := db.GetActiveProjectSyncSettingByTargetParams{
		Provider: "plane", Tenant: "general", Project: "project-1",
	}
	if p.Provider != "plane" || p.Tenant != "general" || p.Project != "project-1" {
		t.Error("unexpected params")
	}
}

func TestDeactivateProjectSyncSettingParams(t *testing.T) {
	p := db.DeactivateProjectSyncSettingParams{
		Provider: "plane", Tenant: "general", Project: "project-1",
	}
	if p.Provider != "plane" || p.Tenant != "general" || p.Project != "project-1" {
		t.Error("unexpected params")
	}
}

func TestErrProjectSyncNotFoundIsDefined(t *testing.T) {
	err := ErrProjectSyncNotFound
	if err == nil {
		t.Fatal("ErrProjectSyncNotFound should not be nil")
	}
	if err.Error() != "project sync setting not found" {
		t.Errorf("error message: got %q", err.Error())
	}
	if err == pgx.ErrNoRows {
		t.Error("ErrProjectSyncNotFound should not equal pgx.ErrNoRows")
	}
}

func TestProjectSyncSettingModelShape(t *testing.T) {
	now := time.Now()
	r := db.ProjectSyncSetting{
		ID: 42, Provider: "plane", Tenant: "general", Project: "project-1",
		GitRemoteUrl: "git@example.com:repo.git", SourceBranch: "dev",
		WorkspaceID: "ws-test", WorkspaceBasePath: "/opt/ws", RepoDirName: "repo",
		Active: true, CreatedAt: now, UpdatedAt: now,
	}
	if r.ID != 42 {
		t.Errorf("ID: got %d", r.ID)
	}
	if !r.Active {
		t.Error("Active should be true")
	}
	if r.WorkspaceBasePath != "/opt/ws" {
		t.Errorf("WorkspaceBasePath: got %q", r.WorkspaceBasePath)
	}
}

// ------ REVIEW_API-1: error boundary tests -

func TestGetActiveProjectSyncSettingByTarget_ReturnsErrProjectSyncNotFound_OnNoRows(t *testing.T) {
	m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {err: pgx.ErrNoRows}}}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	_, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "tenant-1", "proj-1")
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if !errors.Is(err, ErrProjectSyncNotFound) {
		t.Errorf("expected ErrProjectSyncNotFound, got %v", err)
	}
}

func TestGetActiveProjectSyncSettingByTarget_PreservesGenericDBError(t *testing.T) {
	sentinel := errors.New("database timeout")
	m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {err: sentinel}}}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	_, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "tenant-1", "proj-1")
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if !errors.Is(err, sentinel) {
		t.Errorf("expected sentinel DB error %v, got %v", sentinel, err)
	}
}

// ------ G08: Assert storage query args -

func TestCreateProjectSyncRecord_CallsQueryWithCorrectArgs(t *testing.T) {
	expectRow := db.ProjectSyncSetting{ID: 1, Active: true}
	m := &mockDBTX{
		queries: map[string]fakeQueryResult{
			sqlCreateKey:    {row: expectRow},
			sqlGetActiveKey: {err: pgx.ErrNoRows},
			sqlDeactKey:     {err: errors.New("nope")},
		},
	}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	params := db.CreateProjectSyncSettingParams{
		Provider: "bitbucket", Tenant: "acme", Project: "main-app",
		GitRemoteUrl: "git@bitbucket.org:acme/main-app.git", SourceBranch: "main",
		WorkspaceID: "ws-1", WorkspaceBasePath: "/opt/ws", RepoDirName: "main-app",
	}
	result, err := store.CreateProjectSyncRecord(context.Background(), params)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.ID != 1 {
		t.Errorf("expected ID 1, got %d", result.ID)
	}
	if !result.Active {
		t.Error("expected Active=true")
	}
	if len(m.queryLog) == 0 {
		t.Fatal("expected at least one QueryRow call, got none")
	}
	if !sqlContains(m.queryLog, "INSERT INTO project_sync_settings") {
		t.Errorf("expected INSERT query, queryLog=%v", m.queryLog)
	}

	// Assert args for create: [provider, tenant, project, git_remote_url, source_branch, workspace_id, workspace_base_path, repo_dir_name]
	assertArgsAt(t, m.queryCalls, 0, []interface{}{
		"bitbucket", "acme", "main-app",
		"git@bitbucket.org:acme/main-app.git", "main",
		"ws-1", "/opt/ws", "main-app",
	}, "CreateProjectSyncRecord")
}

func TestGetActiveProjectSyncSettingByTarget_CallsQueryWithCorrectArgs(t *testing.T) {
	testRow := db.ProjectSyncSetting{ID: 42, Active: true}
	m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {row: testRow}}}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	_, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "tenant-1", "proj-1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(m.queryCalls) == 0 {
		t.Fatal("expected QueryRow call")
	}

	// Assert args for get: [provider, tenant, project]
	assertArgsAt(t, m.queryCalls, 0, []interface{}{
		"plan", "tenant-1", "proj-1",
	}, "GetActiveProjectSyncSettingByTarget")
}

func TestDeactivateProjectSyncSetting_CallsQueryWithCorrectArgs(t *testing.T) {
	deactivatedRow := db.ProjectSyncSetting{ID: 99, Active: false}
	m := &mockDBTX{queries: map[string]fakeQueryResult{sqlDeactKey: {row: deactivatedRow}}}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	_, err := store.DeactivateProjectSyncSetting(context.Background(), "plan", "t1", "p1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(m.queryCalls) == 0 {
		t.Fatal("expected QueryRow call")
	}

	// Assert args for deactivate: [provider, tenant, project]
	assertArgsAt(t, m.queryCalls, 0, []interface{}{
		"plan", "t1", "p1",
	}, "DeactivateProjectSyncSetting")
}

func TestCreateProjectSyncRecord_GetsNotFoundOnMissingRecord(t *testing.T) {
	m2 := &mockDBTX{
		queries: map[string]fakeQueryResult{
			sqlDeactKey: {err: pgx.ErrNoRows},
		},
	}
	queries2 := db.New(m2)
	store2 := NewStoreWithQueries(queries2)
	_, err := store2.DeactivateProjectSyncSetting(context.Background(), "x", "y", "z")
	// DeactivateProjectSyncSetting does NOT convert ErrNoRows to ErrProjectSyncNotFound.
	// It returns the raw pgx.ErrNoRows.
	if !errors.Is(err, pgx.ErrNoRows) {
		t.Fatalf("expected pgx.ErrNoRows, got %v", err)
	}
	// Verify deactivation was called with the correct args.
	assertArgsAt(t, m2.queryCalls, 0, []interface{}{
		"x", "y", "z",
	}, "DeactivateProjectSyncSetting (after create)")
}

func TestGetActiveProjectSyncSettingByTarget_ReturnsActiveRecord(t *testing.T) {
	testRow := db.ProjectSyncSetting{ID: 42, Active: true}
	m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {row: testRow}}}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	result, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "t1", "p1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.ID != 42 {
		t.Errorf("expected ID 42, got %d", result.ID)
	}
	if len(m.queryLog) == 0 {
		t.Fatal("expected QueryRow call")
	}
	if !sqlContains(m.queryLog, "active = TRUE") {
		t.Error("expected active lookup guard (active = TRUE) in query")
	}
}

func TestGetActiveProjectSyncSettingByTarget_InactiveExcluded(t *testing.T) {
	m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {err: pgx.ErrNoRows}}}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	_, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "t1", "p1")
	if err == nil {
		t.Fatal("expected error for inactive/absent config")
	}
	if !errors.Is(err, ErrProjectSyncNotFound) {
		t.Errorf("expected ErrProjectSyncNotFound, got %v", err)
	}
}

func TestDeactivateProjectSyncSetting_CallsQueryWithActiveGuard(t *testing.T) {
	deactivatedRow := db.ProjectSyncSetting{ID: 99, Active: false}
	m := &mockDBTX{queries: map[string]fakeQueryResult{sqlDeactKey: {row: deactivatedRow}}}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	result, err := store.DeactivateProjectSyncSetting(context.Background(), "plan", "t1", "p1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.Active {
		t.Error("expected Active=false after deactivation, got true")
	}
	if len(m.queryLog) == 0 {
		t.Fatal("expected QueryRow call")
	}
	if !sqlContains(m.queryLog, "active = FALSE") {
		t.Error("expected SET active = FALSE guard in deactivate query")
	}
}

func TestStoreCalls_PassProviderTenantProjectParams(t *testing.T) {
	testRow := db.ProjectSyncSetting{ID: 1, Active: true}
	m := &mockDBTX{
		queries: map[string]fakeQueryResult{
			sqlCreateKey:    {row: testRow},
			sqlGetActiveKey: {row: testRow},
			sqlDeactKey:     {row: db.ProjectSyncSetting{ID: 1, Active: false}},
		},
	}
	queries := db.New(m)
	store := NewStoreWithQueries(queries)
	store.CreateProjectSyncRecord(context.Background(), db.CreateProjectSyncSettingParams{
		Provider: "github", Tenant: "org-x", Project: "auth",
		GitRemoteUrl: "git@github.com:x/feat.git", SourceBranch: "main",
		WorkspaceID: "ws-1", WorkspaceBasePath: "/ws", RepoDirName: "feat",
	})
	store.GetActiveProjectSyncSettingByTarget(context.Background(), "github", "org-x", "auth")
	store.DeactivateProjectSyncSetting(context.Background(), "github", "org-x", "auth")
	if len(m.queryCalls) < 3 {
		t.Fatalf("expected at least 3 QueryRow calls, got %d", len(m.queryCalls))
	}
	// Call 0: CreateProjectSyncSetting — args: [provider, tenant, project, git_remote_url, source_branch, workspace_id, workspace_base_path, repo_dir_name]
	assertArgsAt(t, m.queryCalls, 0, []interface{}{
		"github", "org-x", "auth",
		"git@github.com:x/feat.git", "main",
		"ws-1", "/ws", "feat",
	}, "CreateProjectSyncRecord")
	// Call 1: GetActiveProjectSyncSettingByTarget — args: [provider, tenant, project]
	assertArgsAt(t, m.queryCalls, 1, []interface{}{
		"github", "org-x", "auth",
	}, "GetActiveProjectSyncSettingByTarget")
	// Call 2: DeactivateProjectSyncSetting — args: [provider, tenant, project]
	assertArgsAt(t, m.queryCalls, 2, []interface{}{
		"github", "org-x", "auth",
	}, "DeactivateProjectSyncSetting")
}
