package packs

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"io"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
)

func TestStandardActionSatelliteOperationalFailuresAreNotPersistenceDrift(t *testing.T) {
	tests := []struct {
		name  string
		query string
		run   func(*sql.DB, *[]string) error
	}{
		{
			name:  "schema",
			query: "SELECT DataType",
			run: func(db *sql.DB, drift *[]string) error {
				return verifyStandardActionSchemaRows(db, drift, "action-id", StandardActionDef{
					Name:   "sendRecordEmail",
					Inputs: []StandardActionSchemaDef{{APIName: "attachments"}},
				})
			},
		},
		{
			name:  "stale schema inventory",
			query: "SELECT Direction, APIName",
			run: func(db *sql.DB, drift *[]string) error {
				return verifyNoStaleStandardActionRows(
					db, drift, tableActionSchema, "action-id", map[string]StandardActionSchemaDef{}, true,
					func(rows *sql.Rows) (string, string, error) {
						var direction, apiName string
						if err := rows.Scan(&direction, &apiName); err != nil {
							return "", "", err
						}
						return direction + "\x00" + apiName, direction + "/" + apiName, nil
					},
					"Direction, APIName", "sendRecordEmail", "schema",
				)
			},
		},
		{
			name:  "field default",
			query: "SELECT ValueFormula",
			run: func(db *sql.DB, drift *[]string) error {
				return verifyStandardActionFieldDefaultRows(db, drift, "action-id", StandardActionDef{
					Name:          "sendRecordEmail",
					FieldDefaults: []StandardActionDefaultDef{{TargetObject: "Contact", FieldAPIName: "Email"}},
				})
			},
		},
		{
			name:  "input guard",
			query: "SELECT RequiredPermission",
			run: func(db *sql.DB, drift *[]string) error {
				return verifyStandardActionInputGuardRows(db, drift, "action-id", StandardActionDef{
					Name:        "sendRecordEmail",
					InputGuards: []StandardActionGuardDef{{Condition: "true"}},
				})
			},
		},
		{
			name:  "internal access scope",
			query: "SELECT Reason",
			run: func(db *sql.DB, drift *[]string) error {
				return verifyStandardActionInternalAccessScopeRows(db, drift, "action-id", StandardActionDef{
					Name:                 "sendRecordEmail",
					InternalAccessScopes: []StandardActionInternalAccessScopeDef{{ObjectAPIName: "ContentVersion", Operation: "read"}},
				})
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("sqlmock.New: %v", err)
			}
			defer db.Close()
			operationalErr := errors.New("database unavailable")
			mock.ExpectQuery(tt.query).WillReturnError(operationalErr)

			var drift []string
			err = tt.run(db, &drift)
			if err == nil {
				t.Fatal("verify error = nil, want operational failure")
			}
			if IsStandardActionPersistenceDrift(err) {
				t.Fatalf("operational error was classified as persistence drift: %v", err)
			}
			if len(drift) != 0 {
				t.Fatalf("operational error was accumulated as deterministic drift: %v", drift)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("sql expectations: %v", err)
			}
		})
	}
}

const standardActionRendererTestDriverName = "nexuscrm_standard_action_renderer_test"

func init() {
	sql.Register(standardActionRendererTestDriverName, standardActionRendererTestDriver{})
}

func TestStandardActionRendererIDIsNotCustomizationProtected(t *testing.T) {
	db, err := sql.Open(standardActionRendererTestDriverName, "")
	if err != nil {
		t.Fatalf("open test db: %v", err)
	}
	defer db.Close()

	def := StandardActionDef{
		Name:              "convertLead",
		Label:             "Convert Lead",
		FlowDeveloperName: "Std_ConvertLead",
		RendererID:        "app-pack:crm/action-renderer/lead-convert",
		TargetObjects:     []string{"Lead"},
	}

	if err := upsertStandardAction(db, def, "2026-05-01 00:00:00", "salesforce-crm-standard"); err != nil {
		t.Fatalf("upsert standard action: %v", err)
	}

	err = VerifyStandardActionsForOwnerFromPayload(db, "salesforce-crm-standard", []byte(`[{
		"name": "convertLead",
		"label": "Convert Lead",
		"flowDeveloperName": "Std_ConvertLead",
		"targetObjects": ["Lead"],
		"rendererId": "app-pack:crm/action-renderer/lead-convert"
	}]`))
	if err == nil {
		t.Fatal("VerifyStandardActionsForOwnerFromPayload() error = nil")
	}
	if !IsStandardActionPersistenceDrift(err) {
		t.Fatalf("verify error type = %T, want deterministic standard-action persistence drift", err)
	}
	if !strings.Contains(err.Error(), `RendererId = ""`) {
		t.Fatalf("verify error = %v, want stale RendererId drift even on IsCustomized=1 row", err)
	}
}

func TestValidateStandardActionsRejectsInlineCustomPermissionAuthority(t *testing.T) {
	defs := []StandardActionDef{
		actionWithCustomPermission("firstAction", "SharedPermission", "Shared Permission", "same description"),
	}

	err := ValidateStandardActionDefs(defs)
	if err == nil {
		t.Fatal("ValidateStandardActionDefs() error = nil")
	}
	if !apperrors.IsValidation(err) {
		t.Fatalf("ValidateStandardActionDefs() error type = %T, want validation error", err)
	}
	assertErrorContains(t, err, "custom_permission", "SharedPermission")
}

func TestStandardActionLifecycleRejectsInlineCustomPermissionBeforeDatabaseAccess(t *testing.T) {
	raw := []byte(`[
		{
			"name": "sendRecordEmail",
			"label": "Send Email",
			"flowDeveloperName": "Std_SendRecordEmail",
			"customPermissions": [{
				"apiName": "SendRecordEmail",
				"label": "Send Record Email"
			}]
		}
	]`)
	tests := []struct {
		name string
		run  func(*sql.DB) error
	}{
		{
			name: "apply",
			run: func(db *sql.DB) error {
				return InitializeStandardActionsForOwnerFromPayloads(db, "salesforce-crm-standard", raw, nil)
			},
		},
		{
			name: "verify",
			run: func(db *sql.DB) error {
				return VerifyStandardActionsForOwnerFromPayload(db, "salesforce-crm-standard", raw)
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("open sqlmock: %v", err)
			}
			defer db.Close()

			err = tt.run(db)
			if err == nil {
				t.Fatalf("%s error = nil", tt.name)
			}
			if !apperrors.IsValidation(err) {
				t.Fatalf("%s error type = %T, want validation error: %v", tt.name, err, err)
			}
			if IsStandardActionPersistenceDrift(err) {
				t.Fatalf("%s validation error was misclassified as persisted projection drift: %v", tt.name, err)
			}
			assertErrorContains(t, err, "custom_permission", "SendRecordEmail")
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("%s accessed the database before source validation: %v", tt.name, err)
			}
		})
	}
}

func TestParseStandardActionHeaderFallbackEligibleDefaultsToTrue(t *testing.T) {
	defs, err := ParseStandardActionDefs([]byte(`[
		{"name": "legacyAction", "label": "Legacy Action", "flowDeveloperName": "Std_Legacy"},
		{"name": "quietAction", "label": "Quiet Action", "flowDeveloperName": "Std_Quiet", "headerFallbackEligible": false}
	]`))
	if err != nil {
		t.Fatalf("ParseStandardActionDefs() error = %v", err)
	}
	if got := standardActionHeaderFallbackEligible(defs[0]); !got {
		t.Fatalf("missing headerFallbackEligible default = false, want true")
	}
	if got := standardActionHeaderFallbackEligible(defs[1]); got {
		t.Fatalf("explicit headerFallbackEligible=false defaulted to true")
	}
	if defs[1].HeaderFallbackEligible == nil {
		t.Fatalf("explicit headerFallbackEligible=false was not preserved on the parsed def")
	}
}

func TestUpsertActionSchemaPersistsCollectionAuthority(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	const now = "2026-08-30 01:00:00"
	mock.ExpectExec("INSERT INTO _System_ActionSchema").WithArgs(
		actionSchemaID("action-1", "input", "attachments"),
		"action-1", "input", "attachments", "id",
		nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
		1, 0, nil, 80, now, now,
	).WillReturnResult(sqlmock.NewResult(1, 1))

	_, err = upsertActionSchemaRows(db, "action-1", "input", []StandardActionSchemaDef{{
		APIName: "attachments", DataType: "id", IsCollection: true, SortOrder: 80,
	}}, now)
	if err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestStandardActionsCollectionAuthorityInvalidatesLegacyWarmInstallHash(t *testing.T) {
	raw := []byte(`[{"name":"sendRecordEmail"}]`)
	legacyPayload := append([]byte("v8_header_fallback_eligible\n"), raw...)
	legacyHash := sha256Hex(legacyPayload)
	if got := standardActionsPackHashFromPayload(raw); got == legacyHash {
		t.Fatalf("collection-aware seed hash = legacy v8 hash %q; warm installs would skip schema replay", got)
	}
}

func TestStandardActionsPersistenceReceiptUsesCurrentSeederSemantics(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()

	const (
		now  = "2026-08-30 03:00:00"
		hash = "a1ea225efdf7f7cd62d5d94f3ab4fab00887d0f50b2deb24808015468e9aab3c"
	)
	mock.ExpectQuery("SELECT `Phase`, `Status` FROM `_System_BootstrapState`").
		WithArgs(standardActionsPersistenceStateKey).
		WillReturnRows(sqlmock.NewRows([]string{"Phase", "Status"}).AddRow(hash, "applied"))
	applied, err := StandardActionsPersistenceSemanticsApplied(db)
	if err != nil || !applied {
		t.Fatalf("StandardActionsPersistenceSemanticsApplied() = %v, %v; want true, nil", applied, err)
	}
	mock.ExpectExec("INSERT INTO `_System_BootstrapState`").WithArgs(
		standardActionsPersistenceStateKey, now, hash, "applied", "", now, now,
	).WillReturnResult(sqlmock.NewResult(1, 1))
	if err := MarkStandardActionsPersistenceSemanticsApplied(db, now); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

type standardActionRendererTestDriver struct{}

func (standardActionRendererTestDriver) Open(string) (driver.Conn, error) {
	return standardActionRendererTestConn{}, nil
}

type standardActionRendererTestConn struct{}

func (standardActionRendererTestConn) Prepare(string) (driver.Stmt, error) {
	return nil, driver.ErrSkip
}

func (standardActionRendererTestConn) Close() error {
	return nil
}

func (standardActionRendererTestConn) Begin() (driver.Tx, error) {
	return nil, driver.ErrSkip
}

func (standardActionRendererTestConn) ExecContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Result, error) {
	if strings.Contains(query, "RendererId") && strings.Contains(query, "IF(IsCustomized = 0") && strings.Contains(query, "VALUES(RendererId), Label)") {
		return nil, errUnexpectedCustomizationProtectedRendererID{}
	}
	if !strings.Contains(query, "RendererId") || !strings.Contains(query, "= VALUES(RendererId)") {
		return nil, errMissingUnconditionalRendererID{}
	}
	return driver.RowsAffected(1), nil
}

func (standardActionRendererTestConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
	switch {
	case strings.Contains(query, "SELECT Label, Description, FlowDeveloperName"):
		return &standardActionRendererRows{
			columns: []string{
				"Label", "Description", "FlowDeveloperName", "Category", "RequiredPermissions",
				"TargetObjects", "RendererId", "IsAtomic", "SortOrder", "IsCustomized",
				"IsActive", "IsDeleted", "OwnerPack", "VisibilityCondition",
				"PreviewFlowDeveloperName", "HeaderFallbackEligible",
			},
			values: [][]driver.Value{{
				"Customized Convert",
				"custom description",
				"Std_Customized",
				"CustomCategory",
				"[]",
				`["Lead"]`,
				"",
				int64(0),
				int64(0),
				true,
				true,
				false,
				"salesforce-crm-standard",
				nil,
				nil,
				true,
			}},
		}, nil
	case strings.Contains(query, "SELECT Id FROM `_System_StandardAction` WHERE Name = ?"):
		return &standardActionRendererRows{
			columns: []string{"Id"},
			values:  [][]driver.Value{{"sa_convertlead"}},
		}, nil
	case strings.Contains(query, "SELECT Name") && strings.Contains(query, "_System_StandardAction"):
		return &standardActionRendererRows{
			columns: []string{"Name"},
			values:  [][]driver.Value{{"convertLead"}},
		}, nil
	default:
		return &standardActionRendererRows{}, nil
	}
}

type standardActionRendererRows struct {
	columns []string
	values  [][]driver.Value
	index   int
}

func (r *standardActionRendererRows) Columns() []string {
	return r.columns
}

func (r *standardActionRendererRows) Close() error {
	return nil
}

func (r *standardActionRendererRows) Next(dest []driver.Value) error {
	if r.index >= len(r.values) {
		return io.EOF
	}
	copy(dest, r.values[r.index])
	r.index++
	return nil
}

type errUnexpectedCustomizationProtectedRendererID struct{}

func (errUnexpectedCustomizationProtectedRendererID) Error() string {
	return "RendererId update is still protected by IsCustomized"
}

type errMissingUnconditionalRendererID struct{}

func (errMissingUnconditionalRendererID) Error() string {
	return "RendererId update is not unconditional"
}

var _ driver.ExecerContext = standardActionRendererTestConn{}
var _ driver.QueryerContext = standardActionRendererTestConn{}

func actionWithCustomPermission(actionName, apiName, label, description string) StandardActionDef {
	return StandardActionDef{
		Name:              actionName,
		FlowDeveloperName: "Std_Test",
		CustomPermissions: []StandardActionCustomPermissionDef{{
			APIName:     apiName,
			Label:       label,
			Description: description,
		}},
	}
}

func assertErrorContains(t *testing.T, err error, needles ...string) {
	t.Helper()
	message := err.Error()
	for _, needle := range needles {
		if !strings.Contains(message, needle) {
			t.Fatalf("error %q does not contain %q", message, needle)
		}
	}
}

// TestVerifyStandardActions_PreviewFlowDeveloperNameDriftIsReported asserts
// that setup verify catches drift on PreviewFlowDeveloperName for a
// non-customized (IsCustomized=0) row. Without this coverage the writer can
// seed a PreviewFlowDeveloperName the pack no longer declares (or fail to
// seed one it does), and verify would silently pass — which is the gap
// Codex flagged for issue #945.
const previewFlowDriftTestDriverName = "nexuscrm_preview_flow_drift_test"

func init() {
	sql.Register(previewFlowDriftTestDriverName, previewFlowDriftTestDriver{})
}

type previewFlowDriftTestDriver struct{}

func (previewFlowDriftTestDriver) Open(string) (driver.Conn, error) {
	return previewFlowDriftTestConn{}, nil
}

type previewFlowDriftTestConn struct{}

func (previewFlowDriftTestConn) Prepare(string) (driver.Stmt, error) {
	return nil, driver.ErrSkip
}

func (previewFlowDriftTestConn) Close() error { return nil }

func (previewFlowDriftTestConn) Begin() (driver.Tx, error) { return nil, driver.ErrSkip }

func (previewFlowDriftTestConn) ExecContext(_ context.Context, _ string, _ []driver.NamedValue) (driver.Result, error) {
	return driver.RowsAffected(1), nil
}

func (previewFlowDriftTestConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
	switch {
	case strings.Contains(query, "SELECT Label, Description, FlowDeveloperName"):
		// Non-customized row with a stale PreviewFlowDeveloperName.
		// All other columns are intentionally pack-matching so the only
		// drift the verifier should report is the preview flow name.
		return &standardActionRendererRows{
			columns: []string{
				"Label", "Description", "FlowDeveloperName", "Category", "RequiredPermissions",
				"TargetObjects", "RendererId", "IsAtomic", "SortOrder", "IsCustomized",
				"IsActive", "IsDeleted", "OwnerPack", "VisibilityCondition",
				"PreviewFlowDeveloperName", "HeaderFallbackEligible",
			},
			values: [][]driver.Value{{
				"Convert Lead",
				"",
				"Std_ConvertLead",
				"",
				"[]",
				`["Lead"]`,
				"app-pack:crm/action-renderer/lead-convert",
				int64(0),
				int64(0),
				false, // IsCustomized = 0 — non-customized comparison must run
				true,
				false,
				"salesforce-crm-standard",
				nil,
				"Std_StalePreviewFlow", // stale; pack declares "Std_PreviewLeadConvert"
				true,
			}},
		}, nil
	case strings.Contains(query, "SELECT Id FROM `_System_StandardAction` WHERE Name = ?"):
		return &standardActionRendererRows{
			columns: []string{"Id"},
			values:  [][]driver.Value{{"sa_convertlead"}},
		}, nil
	case strings.Contains(query, "SELECT Name") && strings.Contains(query, "_System_StandardAction"):
		return &standardActionRendererRows{
			columns: []string{"Name"},
			values:  [][]driver.Value{{"convertLead"}},
		}, nil
	default:
		return &standardActionRendererRows{}, nil
	}
}

var _ driver.ExecerContext = previewFlowDriftTestConn{}
var _ driver.QueryerContext = previewFlowDriftTestConn{}

func TestVerifyStandardActions_PreviewFlowDeveloperNameDriftIsReported(t *testing.T) {
	db, err := sql.Open(previewFlowDriftTestDriverName, "")
	if err != nil {
		t.Fatalf("open test db: %v", err)
	}
	defer db.Close()

	err = VerifyStandardActionsForOwnerFromPayload(db, "salesforce-crm-standard", []byte(`[{
		"name": "convertLead",
		"label": "Convert Lead",
		"flowDeveloperName": "Std_ConvertLead",
		"targetObjects": ["Lead"],
		"rendererId": "app-pack:crm/action-renderer/lead-convert",
		"previewFlowDeveloperName": "Std_PreviewLeadConvert"
	}]`))
	if err == nil {
		t.Fatal("VerifyStandardActionsForOwnerFromPayload() error = nil; want PreviewFlowDeveloperName drift")
	}
	if !strings.Contains(err.Error(), "PreviewFlowDeveloperName") {
		t.Fatalf("verify error = %v, want PreviewFlowDeveloperName drift", err)
	}
	if !strings.Contains(err.Error(), "Std_StalePreviewFlow") || !strings.Contains(err.Error(), "Std_PreviewLeadConvert") {
		t.Fatalf("verify error = %v, want stale value and want value to both surface", err)
	}
}

func TestVerifyStandardActions_HeaderFallbackEligibleDriftIsReported(t *testing.T) {
	db, err := sql.Open(previewFlowDriftTestDriverName, "")
	if err != nil {
		t.Fatalf("open test db: %v", err)
	}
	defer db.Close()

	err = VerifyStandardActionsForOwnerFromPayload(db, "salesforce-crm-standard", []byte(`[{
		"name": "convertLead",
		"label": "Convert Lead",
		"flowDeveloperName": "Std_ConvertLead",
		"targetObjects": ["Lead"],
		"rendererId": "app-pack:crm/action-renderer/lead-convert",
		"previewFlowDeveloperName": "Std_StalePreviewFlow",
		"headerFallbackEligible": false
	}]`))
	if err == nil {
		t.Fatal("VerifyStandardActionsForOwnerFromPayload() error = nil; want HeaderFallbackEligible drift")
	}
	if !strings.Contains(err.Error(), "HeaderFallbackEligible") {
		t.Fatalf("verify error = %v, want HeaderFallbackEligible drift", err)
	}
	if !strings.Contains(err.Error(), "true") || !strings.Contains(err.Error(), "false") {
		t.Fatalf("verify error = %v, want live and desired values to both surface", err)
	}
}
