package tenantoverlay

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

	"github.com/DATA-DOG/go-sqlmock"

	domainschema "github.com/nexuscrm/backend/internal/domain/schema"
	"github.com/nexuscrm/shared/pkg/models"
)

// TestVerifyInsertRulesLanded_HappyPath exercises both closure branches:
// ChildCloneRules byte-equal (with order shuffled on the DB side to prove
// canonical sort) and ObjectFieldMapping per-row business-key existence
// (one SELECT 1 per seed row).
func TestVerifyInsertRulesLanded_HappyPath(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	// Map iteration order in verifyChildCloneRulesLanded is
	// nondeterministic (Go map ranging); the test wires every expected
	// query without caring about order.
	mock.MatchExpectationsInOrder(false)

	seedObjects, err := testOverlay(t).objectAPINamesWithChildCloneRules()
	if err != nil {
		t.Fatalf("objectAPINamesWithChildCloneRules: %v", err)
	}
	seedMap := map[string][]models.ChildCloneRule{}
	if err := json.Unmarshal(readTestOverlayFile(t, ObjectChildCloneRulesFile), &seedMap); err != nil {
		t.Fatalf("unmarshal seed: %v", err)
	}
	for _, apiName := range seedObjects {
		rules := seedMap[apiName]
		// Shuffle DB-side by reversing to prove canonical sort handles it.
		shuffled := append([]models.ChildCloneRule(nil), rules...)
		for i, j := 0, len(shuffled)-1; i < j; i, j = i+1, j-1 {
			shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
		}
		encoded, _ := json.Marshal(shuffled)
		mock.ExpectQuery(regexp.QuoteMeta("SELECT `ChildCloneRules` FROM `_System_Object`")).
			WithArgs(domainschema.GenerateObjectID(apiName)).
			WillReturnRows(sqlmock.NewRows([]string{"ChildCloneRules"}).AddRow(string(encoded)))
	}

	// One existence check per seed mapping row. Each query takes the
	// five business-key/owner args (SourceObject, TargetObject,
	// ActionType, SourceField, Source) and returns 1 when a matching
	// authoritative row exists. The returned value is discarded by the
	// closure; all it needs is "row exists" vs sql.ErrNoRows.
	seedGroups := decodeFieldMappingGroupsForTest(t)
	owner := testOverlay(t).objectFieldMappingOwner()
	for _, g := range seedGroups {
		for _, m := range g.Mappings {
			mock.ExpectQuery("SELECT 1 FROM _System_ObjectFieldMapping").
				WithArgs(g.SourceObject, g.TargetObject, g.ActionType, m.SourceField, owner).
				WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
		}
	}

	if err := testOverlay(t).VerifyInsertRulesLanded(context.Background(), db); err != nil {
		t.Fatalf("VerifyInsertRulesLanded on happy path: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations: %v", err)
	}
}

// TestVerifyInsertRulesLanded_HappyPath_AdminOverrideSurvives proves the
// closure passes even when DB row's TargetField / DefaultValue differ
// from the seed — admin overrides (IsCustomized=1) must not trigger
// closure failure. The existence-only check is what enables this:
// closure doesn't read TargetField at all.
func TestVerifyInsertRulesLanded_HappyPath_AdminOverrideSurvives(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	mock.MatchExpectationsInOrder(false)

	// ChildCloneRules side — pass through unchanged so we focus on
	// ObjectFieldMapping behavior.
	seedObjects, _ := testOverlay(t).objectAPINamesWithChildCloneRules()
	seedMap := map[string][]models.ChildCloneRule{}
	_ = json.Unmarshal(readTestOverlayFile(t, ObjectChildCloneRulesFile), &seedMap)
	for _, apiName := range seedObjects {
		encoded, _ := json.Marshal(seedMap[apiName])
		mock.ExpectQuery(regexp.QuoteMeta("SELECT `ChildCloneRules` FROM `_System_Object`")).
			WithArgs(domainschema.GenerateObjectID(apiName)).
			WillReturnRows(sqlmock.NewRows([]string{"ChildCloneRules"}).AddRow(string(encoded)))
	}

	// For every seed row, return `1` from the existence query — we
	// don't care what values the DB row actually has. An admin-edited
	// row (TargetField=X in seed, TargetField=Y in DB, IsCustomized=1)
	// still matches the business-key SELECT and counts as landed.
	seedGroups := decodeFieldMappingGroupsForTest(t)
	owner := testOverlay(t).objectFieldMappingOwner()
	for _, g := range seedGroups {
		for _, m := range g.Mappings {
			mock.ExpectQuery("SELECT 1 FROM _System_ObjectFieldMapping").
				WithArgs(g.SourceObject, g.TargetObject, g.ActionType, m.SourceField, owner).
				WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
		}
	}

	if err := testOverlay(t).VerifyInsertRulesLanded(context.Background(), db); err != nil {
		t.Fatalf("VerifyInsertRulesLanded should pass with admin overrides: %v", err)
	}
}

// TestVerifyInsertRulesLanded_ChildCloneMissingObjectRow asserts that
// the closure hard-fails when the tenant-objects apply step hasn't
// created the parent _System_Object row.
func TestVerifyInsertRulesLanded_ChildCloneMissingObjectRow(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	seedObjects, err := testOverlay(t).objectAPINamesWithChildCloneRules()
	if err != nil {
		t.Fatalf("objectAPINamesWithChildCloneRules: %v", err)
	}
	if len(seedObjects) == 0 {
		t.Skip("no ChildCloneRules seeded — nothing to assert")
	}
	missing := seedObjects[0]

	seedMap := map[string][]models.ChildCloneRule{}
	_ = json.Unmarshal(readTestOverlayFile(t, ObjectChildCloneRulesFile), &seedMap)
	mock.MatchExpectationsInOrder(false)
	for _, apiName := range seedObjects {
		id := domainschema.GenerateObjectID(apiName)
		if apiName == missing {
			mock.ExpectQuery(regexp.QuoteMeta("SELECT `ChildCloneRules` FROM `_System_Object`")).
				WithArgs(id).
				WillReturnRows(sqlmock.NewRows([]string{"ChildCloneRules"})) // empty
			continue
		}
		encoded, _ := json.Marshal(seedMap[apiName])
		mock.ExpectQuery(regexp.QuoteMeta("SELECT `ChildCloneRules` FROM `_System_Object`")).
			WithArgs(id).
			WillReturnRows(sqlmock.NewRows([]string{"ChildCloneRules"}).AddRow(string(encoded)))
	}

	err = testOverlay(t).VerifyInsertRulesLanded(context.Background(), db)
	if err == nil {
		t.Fatalf("expected closure failure for missing object row, got nil")
	}
	if !strings.Contains(err.Error(), missing) {
		t.Fatalf("error message should name the missing object %q; got %v", missing, err)
	}
}

// TestVerifyInsertRulesLanded_ChildCloneDrift asserts byte-level
// canonical mismatch on the ChildCloneRules column surfaces as a
// failure with both SEED and DB canonical forms in the error.
// (ChildCloneRules is tenant-overlay-owned per Commit 7 contract;
// byte-equal is the correct closure for this surface.)
func TestVerifyInsertRulesLanded_ChildCloneDrift(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	seedObjects, err := testOverlay(t).objectAPINamesWithChildCloneRules()
	if err != nil {
		t.Fatalf("objectAPINamesWithChildCloneRules: %v", err)
	}
	if len(seedObjects) == 0 {
		t.Skip("no ChildCloneRules seeded")
	}
	drifted := seedObjects[0]

	seedMap := map[string][]models.ChildCloneRule{}
	_ = json.Unmarshal(readTestOverlayFile(t, ObjectChildCloneRulesFile), &seedMap)

	mock.MatchExpectationsInOrder(false)
	for _, apiName := range seedObjects {
		id := domainschema.GenerateObjectID(apiName)
		rules := seedMap[apiName]
		if apiName == drifted {
			if len(rules) == 0 {
				continue
			}
			rules = rules[1:]
		}
		encoded, _ := json.Marshal(rules)
		mock.ExpectQuery(regexp.QuoteMeta("SELECT `ChildCloneRules` FROM `_System_Object`")).
			WithArgs(id).
			WillReturnRows(sqlmock.NewRows([]string{"ChildCloneRules"}).AddRow(string(encoded)))
	}

	err = testOverlay(t).VerifyInsertRulesLanded(context.Background(), db)
	if err == nil {
		t.Fatalf("expected closure failure on drifted rules, got nil")
	}
	if !strings.Contains(err.Error(), drifted) ||
		!strings.Contains(err.Error(), "SEED (canonical)") ||
		!strings.Contains(err.Error(), "DB   (canonical)") {
		t.Fatalf("error should name %q AND include SEED/DB canonical forms; got %v", drifted, err)
	}
}

// TestVerifyInsertRulesLanded_ObjectFieldMappingBusinessKeyMissing
// replaces the round-6 "value drift" test. Under the new
// business-key-existence semantics, drift is only detectable when the
// business key has no active, non-deleted row in DB — simulating a
// silent write-miss or a rogue delete. Value differences (admin
// override) are by design not drift.
func TestVerifyInsertRulesLanded_ObjectFieldMappingBusinessKeyMissing(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	mock.MatchExpectationsInOrder(false)

	// Stub ChildCloneRules happy path so we fall through to mappings.
	seedObjects, _ := testOverlay(t).objectAPINamesWithChildCloneRules()
	seedMap := map[string][]models.ChildCloneRule{}
	_ = json.Unmarshal(readTestOverlayFile(t, ObjectChildCloneRulesFile), &seedMap)
	for _, apiName := range seedObjects {
		encoded, _ := json.Marshal(seedMap[apiName])
		mock.ExpectQuery(regexp.QuoteMeta("SELECT `ChildCloneRules` FROM `_System_Object`")).
			WithArgs(domainschema.GenerateObjectID(apiName)).
			WillReturnRows(sqlmock.NewRows([]string{"ChildCloneRules"}).AddRow(string(encoded)))
	}

	seedGroups := decodeFieldMappingGroupsForTest(t)
	if len(seedGroups) == 0 || len(seedGroups[0].Mappings) == 0 {
		t.Skip("no seed mappings to drift")
	}
	// Pick one seed mapping to simulate as missing. Every other mapping's
	// existence query returns 1.
	missingGroup := seedGroups[0]
	missingMapping := missingGroup.Mappings[0]
	owner := testOverlay(t).objectFieldMappingOwner()
	for _, g := range seedGroups {
		for _, m := range g.Mappings {
			q := mock.ExpectQuery("SELECT 1 FROM _System_ObjectFieldMapping").
				WithArgs(g.SourceObject, g.TargetObject, g.ActionType, m.SourceField, owner)
			if g.SourceObject == missingGroup.SourceObject &&
				g.TargetObject == missingGroup.TargetObject &&
				g.ActionType == missingGroup.ActionType &&
				m.SourceField == missingMapping.SourceField {
				// Missing — return no rows (sql.ErrNoRows).
				q.WillReturnRows(sqlmock.NewRows([]string{"1"}))
				continue
			}
			q.WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
		}
	}

	err = testOverlay(t).VerifyInsertRulesLanded(context.Background(), db)
	if err == nil {
		t.Fatalf("expected closure failure on missing business key, got nil")
	}
	if !strings.Contains(err.Error(), missingGroup.SourceObject) ||
		!strings.Contains(err.Error(), missingGroup.TargetObject) ||
		!strings.Contains(err.Error(), missingMapping.SourceField) {
		t.Fatalf("error should name the missing tuple + SourceField; got %v", err)
	}
	if !strings.Contains(err.Error(), "no active, non-deleted tenant-owned row") {
		t.Fatalf("error message should reflect existence-semantics wording; got %v", err)
	}
}

// TestVerifyInsertRulesLanded_ObjectFieldMappingTombstoneOnlyFails
// asserts that a business key with ONLY a soft-deleted row counts as
// missing — existence is gated on IsActive=1 AND IsDeleted=0, which
// is what we implement via SQL. Modelled here by the mock returning
// zero rows for the targeted business key (the SQL WHERE clause
// already excludes tombstones).
func TestVerifyInsertRulesLanded_ObjectFieldMappingTombstoneOnlyFails(t *testing.T) {
	// The mock-level behavior is identical to the "missing" case
	// above — the SQL filters out IsDeleted=1 rows server-side, so
	// the mock returns zero rows either way. This test exists to
	// document the intent; the previous test already exercises the
	// zero-row failure path.
	t.Log("verified by TestVerifyInsertRulesLanded_ObjectFieldMappingBusinessKeyMissing " +
		"— SQL WHERE clause IsActive=1 AND IsDeleted=0 makes tombstones indistinguishable " +
		"from absence from the closure's point of view.")
}

func decodeFieldMappingGroupsForTest(t *testing.T) []fieldMappingGroupForTest {
	t.Helper()
	var groups []fieldMappingGroupForTest
	if err := json.Unmarshal(readTestOverlayFile(t, ObjectFieldMappingsFile), &groups); err != nil {
		t.Fatalf("unmarshal field mapping seed: %v", err)
	}
	return groups
}

// fieldMappingGroupForTest mirrors the seed JSON shape without importing
// the bootstrap package's exported type — keeps the test's dependency
// surface minimal and stable against bootstrap refactors.
type fieldMappingGroupForTest struct {
	SourceObject string                   `json:"sourceObject"`
	TargetObject string                   `json:"targetObject"`
	ActionType   string                   `json:"actionType"`
	Mappings     []fieldMappingDefForTest `json:"mappings"`
}

type fieldMappingDefForTest struct {
	SourceField       string `json:"sourceField"`
	TargetField       string `json:"targetField"`
	DefaultValue      string `json:"defaultValue,omitempty"`
	SourceLookupField string `json:"sourceLookupField,omitempty"`
}
