// Package autonumber implements metadata-driven identifier assignment for NexusCRM.
package autonumber

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"testing"

	"github.com/nexuscrm/shared/pkg/models"
)

// fakeMetadataProvider is a map-backed MetadataProvider for tests.
// Exposes both ordered ListRules and keyed GetRule.
type fakeMetadataProvider struct {
	rules []Rule
}

func (f *fakeMetadataProvider) GetRule(fieldID string) *Rule {
	for i := range f.rules {
		if f.rules[i].FieldID == fieldID {
			r := f.rules[i]
			return &r
		}
	}
	return nil
}

func (f *fakeMetadataProvider) ListRules() []Rule {
	out := make([]Rule, len(f.rules))
	copy(out, f.rules)
	return out
}

// --- fakeDB — an always-erroring *sql.DB for unit tests that never
// reach the reservation path. Necessary because NewEngine rejects a
// nil reservationDB argument. ---

// fakeDriver / fakeConn / fakeStmt implement the minimal driver
// surface; any query or exec returns fakeDBErr.
type fakeDriver struct{}

// fakeDBErr is returned by every fake-DB operation so tests that
// inadvertently reach the SQL layer fail loudly with a recognizable
// sentinel. Tests that exercise reservation use a real DB (see
// reserve_test.go integration tag).
var fakeDBErr = fmt.Errorf("autonumber test fake DB: no real connection")

func (fakeDriver) Open(name string) (driver.Conn, error) {
	return fakeConn{}, nil
}

type fakeConn struct{}

func (fakeConn) Prepare(q string) (driver.Stmt, error)              { return fakeStmt{}, nil }
func (fakeConn) Close() error                                       { return nil }
func (fakeConn) Begin() (driver.Tx, error)                          { return nil, fakeDBErr }
func (fakeConn) BeginTx(_ context.Context, _ driver.TxOptions) (driver.Tx, error) {
	return nil, fakeDBErr
}
func (fakeConn) ExecContext(_ context.Context, _ string, _ []driver.NamedValue) (driver.Result, error) {
	return nil, fakeDBErr
}
func (fakeConn) QueryContext(_ context.Context, _ string, _ []driver.NamedValue) (driver.Rows, error) {
	return nil, fakeDBErr
}
func (fakeConn) Ping(_ context.Context) error { return nil } // Avoids sql.DB re-dial loops.

type fakeStmt struct{}

func (fakeStmt) Close() error                                    { return nil }
func (fakeStmt) NumInput() int                                   { return -1 }
func (fakeStmt) Exec(_ []driver.Value) (driver.Result, error)    { return nil, fakeDBErr }
func (fakeStmt) Query(_ []driver.Value) (driver.Rows, error)     { return nil, fakeDBErr }
func (fakeStmt) ExecContext(_ context.Context, _ []driver.NamedValue) (driver.Result, error) {
	return nil, fakeDBErr
}
func (fakeStmt) QueryContext(_ context.Context, _ []driver.NamedValue) (driver.Rows, error) {
	return nil, fakeDBErr
}

// fakeConnector implements driver.Connector so sql.OpenDB returns a
// DB that never attempts a real network connection.
type fakeConnector struct{}

func (fakeConnector) Connect(_ context.Context) (driver.Conn, error) { return fakeConn{}, nil }
func (fakeConnector) Driver() driver.Driver                          { return fakeDriver{} }

// openFakeDB returns a *sql.DB backed by the fake driver — safe to
// pass into engine options that require a non-nil DB but must not be
// invoked for real SQL in the test body.
func openFakeDB(t *testing.T) *sql.DB {
	t.Helper()
	db := sql.OpenDB(fakeConnector{})
	t.Cleanup(func() { _ = db.Close() })
	return db
}

// --- fakeResolver — map-backed CrossRecordResolver for tests ---

type fakeResolver struct {
	// byChainRoot keyed on fmt.Sprintf("%s|%s", firstStep.TargetObject, fkValue)
	byChainRoot map[string]models.SObject
	// errOnKey opt-in error injection
	errOnKey map[string]error
}

func newFakeResolver() *fakeResolver {
	return &fakeResolver{
		byChainRoot: map[string]models.SObject{},
		errOnKey:    map[string]error{},
	}
}

func (f *fakeResolver) put(targetObject string, id string, row models.SObject) {
	f.byChainRoot[targetObject+"|"+id] = row
}

func (f *fakeResolver) LoadTarget(
	_ context.Context,
	_ string,
	rootRecord models.SObject,
	chain []CrossRecordStep,
) (models.SObject, error) {
	if len(chain) == 0 {
		return nil, fmt.Errorf("fake resolver: empty chain")
	}
	// Walk: step 0 uses rootRecord's FK column; subsequent steps use
	// the previous row's FK.
	cur := rootRecord
	for i, step := range chain {
		fkCol := step.FK
		if fkCol == "" {
			// Simple resolver default — infer "<TargetObject>Id".
			fkCol = step.TargetObject + "Id"
		}
		fkVal, ok := cur[fkCol]
		if !ok || fkVal == nil {
			return nil, fmt.Errorf("%w: FK column %q nil at step %d", ErrTokenResolve, fkCol, i)
		}
		idStr := fmt.Sprintf("%v", fkVal)
		if err, ok := f.errOnKey[step.TargetObject+"|"+idStr]; ok {
			return nil, err
		}
		next, ok := f.byChainRoot[step.TargetObject+"|"+idStr]
		if !ok {
			return nil, fmt.Errorf("%w: target %q row %q missing", ErrTokenResolve, step.TargetObject, idStr)
		}
		cur = next
	}
	return cur, nil
}

// ensure we import io so goimports does not drop the SQL stdlib
// type imports accidentally — the fake stmt returns io.EOF in some
// variants. Not used right now but keeping the import harmlessly.
var _ = io.EOF
