package specdrift

import (
	"errors"
	"fmt"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/shared/pkg/constants"
)

func TestCompareFieldAgainstSpecCoversValidatorMetadata(t *testing.T) {
	spec := fieldSpec{
		APIName:          "Status",
		Type:             "string",
		Createable:       boolPtr(false),
		Updateable:       boolPtr(false),
		ReferenceTo:      []string{"ScopedStatusOption"},
		Validator:        "scoped_value_validator",
		ValidatorConfig:  `{"scope":"CampaignId"}`,
		ControllingField: "ParentId",
	}
	liveReferenceTo := `["OtherStatusTable"]`
	liveValidator := "legacy_validator"
	liveValidatorConfig := `{"scope":"OtherId"}`
	liveControllingField := "OtherId"
	live := liveField{
		typ:              "string",
		createable:       true,
		updateable:       true,
		referenceTo:      &liveReferenceTo,
		validator:        &liveValidator,
		validatorConfig:  &liveValidatorConfig,
		controllingField: &liveControllingField,
	}

	diffs := compareFieldAgainstSpec(spec, live)
	assertDiffContains(t, diffs, "ReferenceTo:")
	assertDiffContains(t, diffs, "Createable:")
	assertDiffContains(t, diffs, "Updateable:")
	assertDiffContains(t, diffs, "Validator:")
	assertDiffContains(t, diffs, "ValidatorConfig:")
	assertDiffContains(t, diffs, "ControllingField:")
	if !isStructuralDrift(spec, live) {
		t.Fatal("validator metadata drift should be structural drift")
	}
}

func TestCompareFieldAgainstSpecAcceptsMatchingValidatorMetadata(t *testing.T) {
	spec := fieldSpec{
		APIName:          "Status",
		Type:             "string",
		Createable:       boolPtr(true),
		Updateable:       boolPtr(true),
		ReferenceTo:      []string{"ScopedStatusOption"},
		Validator:        "scoped_value_validator",
		ValidatorConfig:  `{"scope":"CampaignId"}`,
		ControllingField: "ParentId",
	}
	liveReferenceTo := `["ScopedStatusOption"]`
	liveValidator := "scoped_value_validator"
	liveValidatorConfig := `{"scope":"CampaignId"}`
	liveControllingField := "ParentId"
	live := liveField{
		typ:              "string",
		createable:       true,
		updateable:       true,
		referenceTo:      &liveReferenceTo,
		validator:        &liveValidator,
		validatorConfig:  &liveValidatorConfig,
		controllingField: &liveControllingField,
	}

	if diffs := compareFieldAgainstSpec(spec, live); len(diffs) != 0 {
		t.Fatalf("compareFieldAgainstSpec() diffs = %#v, want none", diffs)
	}
	if isStructuralDrift(spec, live) {
		t.Fatal("matching validator metadata should not be structural drift")
	}
}

// TestCompareFieldAgainstSpecCoversChildRelationshipName covers issue #153
// Track B substrate: a pack-authored childRelationshipName must be checked
// against the live _System_Field column. Without this gate, an admin could
// edit ChildRelationshipName directly in the DB and verify-pack would not
// flag it, breaking the locked-pack manageability invariant.
func TestCompareFieldAgainstSpecCoversChildRelationshipName(t *testing.T) {
	spec := fieldSpec{
		APIName:               "AccountId",
		Type:                  "reference",
		ReferenceTo:           []string{"Account"},
		ChildRelationshipName: "Contacts",
	}
	liveReferenceTo := `["Account"]`

	// Drift case: live value differs from spec.
	liveWrong := "WrongName"
	live := liveField{
		typ:                   "reference",
		referenceTo:           &liveReferenceTo,
		childRelationshipName: &liveWrong,
	}
	diffs := compareFieldAgainstSpec(spec, live)
	assertDiffContains(t, diffs, "ChildRelationshipName:")
	if !isStructuralDrift(spec, live) {
		t.Fatal("ChildRelationshipName drift must be structural")
	}

	// Drift case: spec declares value, live is NULL.
	liveMissing := liveField{
		typ:         "reference",
		referenceTo: &liveReferenceTo,
	}
	if !isStructuralDrift(spec, liveMissing) {
		t.Fatal("missing live ChildRelationshipName must be structural drift when spec declares one")
	}

	// Match case: spec and live agree.
	liveOK := "Contacts"
	matched := liveField{
		typ:                   "reference",
		referenceTo:           &liveReferenceTo,
		childRelationshipName: &liveOK,
	}
	if diffs := compareFieldAgainstSpec(spec, matched); len(diffs) != 0 {
		t.Fatalf("matching ChildRelationshipName should not produce diffs, got %#v", diffs)
	}
	if isStructuralDrift(spec, matched) {
		t.Fatal("matching ChildRelationshipName must not be structural drift")
	}

	// No-spec case: spec declares no ChildRelationshipName (substrate-prep
	// state where pack hasn't authored a value yet); live NULL is the
	// expected resting state and must not register as drift.
	noSpec := fieldSpec{
		APIName:     "AccountId",
		Type:        "reference",
		ReferenceTo: []string{"Account"},
	}
	if isStructuralDrift(noSpec, liveMissing) {
		t.Fatal("absent spec + NULL live must not be structural drift")
	}
}

func TestCompareFieldAgainstSpecCoversSiblingMaxParentField(t *testing.T) {
	spec := fieldSpec{
		APIName:               "SortOrder__c",
		Type:                  "double",
		SiblingMaxParentField: "ParentId",
	}

	liveWrong := "OtherParentId"
	live := liveField{
		typ:                   "double",
		siblingMaxParentField: &liveWrong,
	}
	diffs := compareFieldAgainstSpec(spec, live)
	assertDiffContains(t, diffs, "SiblingMaxParentField:")
	if !isStructuralDrift(spec, live) {
		t.Fatal("SiblingMaxParentField drift must be structural")
	}

	liveMissing := liveField{typ: "double"}
	if !isStructuralDrift(spec, liveMissing) {
		t.Fatal("missing live SiblingMaxParentField must be structural drift when spec declares one")
	}

	liveOK := "ParentId"
	matched := liveField{
		typ:                   "double",
		siblingMaxParentField: &liveOK,
	}
	if diffs := compareFieldAgainstSpec(spec, matched); len(diffs) != 0 {
		t.Fatalf("matching SiblingMaxParentField should not produce diffs, got %#v", diffs)
	}
	if isStructuralDrift(spec, matched) {
		t.Fatal("matching SiblingMaxParentField must not be structural drift")
	}

	noSpec := fieldSpec{
		APIName: "SortOrder__c",
		Type:    "double",
	}
	if isStructuralDrift(noSpec, liveMissing) {
		t.Fatal("absent spec + NULL live must not be structural drift")
	}
	if diffs := compareFieldAgainstSpec(noSpec, live); len(diffs) == 0 {
		t.Fatal("stale live SiblingMaxParentField must produce drift when spec omits it")
	}
}

func TestIsStructuralDriftIncludesRequiredAndDefault(t *testing.T) {
	defaultRaw := []byte(`"existing"`)
	specBase := fieldSpec{APIName: "Subject", Type: "string", Required: true, Default: defaultRaw}
	live := liveField{typ: "string", required: true}
	defaultLive := "existing"
	live.defaultValue = &defaultLive

	if isStructuralDrift(specBase, live) {
		t.Fatal("matching Required + Default should not be structural drift")
	}

	requiredFlipped := live
	requiredFlipped.required = false
	if !isStructuralDrift(specBase, requiredFlipped) {
		t.Fatal("Required mismatch must be structural drift")
	}

	defaultFlipped := live
	wrongDefault := "different"
	defaultFlipped.defaultValue = &wrongDefault
	if !isStructuralDrift(specBase, defaultFlipped) {
		t.Fatal("DefaultValue mismatch must be structural drift")
	}
}

func TestCompareFieldAgainstSpecCoversUnique(t *testing.T) {
	spec := fieldSpec{APIName: "ExternalRemoteId__c", Type: "string", Unique: true}
	live := liveField{typ: "string", unique: false}

	diffs := compareFieldAgainstSpec(spec, live)
	assertDiffContains(t, diffs, "IsUnique:")
	if !isStructuralDrift(spec, live) {
		t.Fatal("unique mismatch must be structural drift")
	}

	live.unique = true
	if diffs := compareFieldAgainstSpec(spec, live); len(diffs) != 0 {
		t.Fatalf("matching unique bit should not produce diffs, got %#v", diffs)
	}
	if isStructuralDrift(spec, live) {
		t.Fatal("matching unique bit must not be structural drift")
	}
}

func TestVerifyDetectsDeleteConstraintDriftFromLiveFieldRows(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT o\\.`ApiName`, f\\.`ApiName`").
		WithArgs("standard_object").
		WillReturnRows(deleteConstraintLiveFieldRows())

	err = Verify(db, []byte(`[
		{"apiName":"Lead","fields":[
			{"apiName":"First_Campaign__c","type":"reference","createable":true,"updateable":true,"referenceTo":["Campaign"],"deleteConstraint":"SetNull"}
		]}
	]`), "delete-constraint-spec", nil, true)
	var structural *StructuralDriftError
	if !errors.As(err, &structural) || len(structural.FieldDrifts) != 1 || len(structural.FieldDrifts[0].Diffs) != 1 {
		t.Fatalf("Verify() error = %#v, want one DeleteConstraint structural drift", err)
	}
	if diff := structural.FieldDrifts[0].Diffs[0]; diff.String() != "DeleteConstraint: spec=SetNull live=NULL" {
		t.Fatalf("DeleteConstraint diff = %#v", diff)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

func deleteConstraintLiveFieldRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"ObjectApiName", "FieldApiName", "Type", "Createable", "Updateable", "Required",
		"DefaultValue", "HelpText", "Options", "ProjectionConfig", "ReferenceTo", "WritableReferenceTo", "DeleteRule",
		"Validator", "ValidatorConfig", "ControllingField", "ChildRelationshipName",
		"SiblingMaxParentField", "WriteProtectionPolicy", "ReadProtectionPolicy", "IsUnique",
		"TrackHistory", "IsCustomized",
	}).AddRow(
		"Lead", "First_Campaign__c", "reference", true, true, false,
		nil, nil, nil, nil, `["Campaign"]`, nil, nil, nil, nil, nil, nil, nil, nil, nil, false, false, false,
	)
}

func TestVerifyTrackHistoryDriftFromLiveFieldRows(t *testing.T) {
	for _, test := range []struct {
		name        string
		specHistory bool
		liveHistory bool
		wantDrift   bool
	}{
		{name: "spec_true_live_false", specHistory: true, liveHistory: false, wantDrift: true},
		{name: "spec_false_live_true", specHistory: false, liveHistory: true, wantDrift: true},
		{name: "values_equal", specHistory: true, liveHistory: true, wantDrift: false},
	} {
		t.Run(test.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("sqlmock.New: %v", err)
			}
			defer db.Close()

			mock.ExpectQuery("SELECT o\\.`ApiName`, f\\.`ApiName`.*f\\.`TrackHistory`").
				WithArgs("standard_object").
				WillReturnRows(trackHistoryLiveFieldRows(test.liveHistory))

			specJSON := []byte(`[{"apiName":"Widget","fields":[{"apiName":"History__c","type":"string","createable":true,"updateable":true,"trackHistory":` + fmt.Sprint(test.specHistory) + `}]}]`)
			err = Verify(db, specJSON, "track-history-spec", nil, true)
			if !test.wantDrift {
				if err != nil {
					t.Fatalf("Verify() error = %v, want matching TrackHistory values", err)
				}
			} else {
				var structural *StructuralDriftError
				if !errors.As(err, &structural) || len(structural.FieldDrifts) != 1 || len(structural.FieldDrifts[0].Diffs) != 1 {
					t.Fatalf("Verify() error = %#v, want one detailed TrackHistory structural drift", err)
				}
				diff := structural.FieldDrifts[0].Diffs[0]
				if diff.Kind != FieldDiffKindTrackHistory || diff.String() != fmt.Sprintf("TrackHistory: spec=%t live=%t", test.specHistory, test.liveHistory) {
					t.Fatalf("TrackHistory diff = %#v, want detailed spec/live values", diff)
				}
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("sqlmock expectations: %v", err)
			}
		})
	}
}

func trackHistoryLiveFieldRows(trackHistory bool) *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"ObjectApiName", "FieldApiName", "Type", "Createable", "Updateable", "Required",
		"DefaultValue", "HelpText", "Options", "ProjectionConfig", "ReferenceTo", "WritableReferenceTo", "DeleteRule",
		"Validator", "ValidatorConfig", "ControllingField", "ChildRelationshipName",
		"SiblingMaxParentField", "WriteProtectionPolicy", "ReadProtectionPolicy", "IsUnique",
		"TrackHistory", "IsCustomized",
	}).AddRow(
		"Widget", "History__c", "string", true, true, false,
		nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, false, trackHistory, false,
	)
}

func TestVerifyInlineHelpTextDriftFromLiveFieldRows(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT o\\.`ApiName`, f\\.`ApiName`.*f\\.`HelpText`").
		WithArgs("standard_object").
		WillReturnRows(inlineHelpTextLiveFieldRows("Stale help text"))

	err = Verify(db, []byte(`[
		{"apiName":"Lead","fields":[
			{"apiName":"Region__c","type":"string","createable":true,"updateable":true,"inlineHelpText":"Assigned from the address country"}
		]}
	]`), "inline-help-text-spec", nil, true)
	var structural *StructuralDriftError
	if !errors.As(err, &structural) || len(structural.FieldDrifts) != 1 || len(structural.FieldDrifts[0].Diffs) != 1 {
		t.Fatalf("Verify() error = %#v, want one HelpText structural drift", err)
	}
	if diff := structural.FieldDrifts[0].Diffs[0]; diff.String() != `HelpText: spec="Assigned from the address country" live="Stale help text"` {
		t.Fatalf("HelpText diff = %#v", diff)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

func inlineHelpTextLiveFieldRows(helpText string) *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"ObjectApiName", "FieldApiName", "Type", "Createable", "Updateable", "Required",
		"DefaultValue", "HelpText", "Options", "ProjectionConfig", "ReferenceTo", "WritableReferenceTo", "DeleteRule",
		"Validator", "ValidatorConfig", "ControllingField", "ChildRelationshipName",
		"SiblingMaxParentField", "WriteProtectionPolicy", "ReadProtectionPolicy", "IsUnique",
		"TrackHistory", "IsCustomized",
	}).AddRow(
		"Lead", "Region__c", "string", true, true, false,
		nil, helpText, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, false, false, false,
	)
}

func TestVerifyDetectsUniqueDriftFromLiveFieldRows(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT o\\.`ApiName`, f\\.`ApiName`").
		WithArgs("standard_object").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName",
			"FieldApiName",
			"Type",
			"Createable",
			"Updateable",
			"Required",
			"DefaultValue",
			"HelpText",
			"Options",
			"ProjectionConfig",
			"ReferenceTo",
			"WritableReferenceTo",
			"DeleteRule",
			"Validator",
			"ValidatorConfig",
			"ControllingField",
			"ChildRelationshipName",
			"SiblingMaxParentField",
			"WriteProtectionPolicy",
			"ReadProtectionPolicy",
			"IsUnique",
			"TrackHistory",
			"IsCustomized",
		}).AddRow(
			"Widget",
			"ExternalRemoteId__c",
			"string",
			true,
			true,
			false,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			false,
			false,
			false,
		))

	err = Verify(db, []byte(`[
		{
			"apiName": "Widget",
			"fields": [
				{
					"apiName": "ExternalRemoteId__c",
					"type": "string",
					"createable": true,
					"updateable": true,
					"unique": true
				}
			]
		}
	]`), "unique-spec", nil, true)
	if err == nil {
		t.Fatal("Verify = nil, want unique drift")
	}
	if !strings.Contains(err.Error(), "unique-spec: 1 structural drift") {
		t.Fatalf("Verify error = %v, want unique structural drift", err)
	}
	if !IsUniqueOnlyFieldDrift(err) {
		t.Fatalf("Verify error = %v, want typed unique-only field drift", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

func TestVerifyUniqueDriftFollowsFieldCustomizationPolicy(t *testing.T) {
	specJSON := []byte(`[
		{
			"apiName": "Widget",
			"fields": [
				{
					"apiName": "ExternalRemoteId__c",
					"type": "string",
					"createable": true,
					"updateable": true,
					"unique": true
				}
			]
		}
	]`)

	t.Run("defaults_only customized unique override is warning-only", func(t *testing.T) {
		db, mock, err := sqlmock.New()
		if err != nil {
			t.Fatalf("sqlmock.New: %v", err)
		}
		defer db.Close()
		expectUniqueDriftLiveFieldRow(t, mock, false, true)

		err = VerifyForTableTypes(db, specJSON, "unique-spec", nil, true, CustomizationPolicyAllow, constants.TableTypeStandardObject)
		if err != nil {
			t.Fatalf("VerifyForTableTypes = %v, want nil for customized defaults_only unique override", err)
		}
		if err := mock.ExpectationsWereMet(); err != nil {
			t.Fatalf("sqlmock expectations: %v", err)
		}
	})

	t.Run("locked customized unique drift is fatal", func(t *testing.T) {
		db, mock, err := sqlmock.New()
		if err != nil {
			t.Fatalf("sqlmock.New: %v", err)
		}
		defer db.Close()
		expectUniqueDriftLiveFieldRow(t, mock, false, true)

		err = VerifyForTableTypes(db, specJSON, "unique-spec", nil, true, CustomizationPolicyDrift, constants.TableTypeStandardObject)
		if err == nil {
			t.Fatal("VerifyForTableTypes = nil, want fatal unique drift for locked customized field")
		}
		if !strings.Contains(err.Error(), "unique-spec: 1 structural drift") {
			t.Fatalf("VerifyForTableTypes error = %v, want unique structural drift", err)
		}
		if !IsUniqueOnlyFieldDrift(err) {
			t.Fatalf("VerifyForTableTypes error = %v, want typed unique-only field drift", err)
		}
		if err := mock.ExpectationsWereMet(); err != nil {
			t.Fatalf("sqlmock expectations: %v", err)
		}
	})
}

func TestCustomizationPolicyGatingMatrix(t *testing.T) {
	cases := []struct {
		name         string
		policy       CustomizationPolicy
		isCustomized bool
		wantAllow    bool
	}{
		{"locked + customized: no escape", CustomizationPolicyDrift, true, false},
		{"locked + non-customized: no escape", CustomizationPolicyDrift, false, false},
		{"defaults_only + customized: escape", CustomizationPolicyAllow, true, true},
		{"defaults_only + non-customized: no escape", CustomizationPolicyAllow, false, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := customizationPolicyAllowsLiveOverride(tc.policy, tc.isCustomized)
			if got != tc.wantAllow {
				t.Fatalf("customizationPolicyAllowsLiveOverride(%v, %v) = %v, want %v",
					tc.policy, tc.isCustomized, got, tc.wantAllow)
			}
		})
	}
}

func expectUniqueDriftLiveFieldRow(t *testing.T, mock sqlmock.Sqlmock, liveUnique bool, isCustomized bool) {
	t.Helper()
	mock.ExpectQuery("SELECT o\\.`ApiName`, f\\.`ApiName`").
		WithArgs("standard_object").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName",
			"FieldApiName",
			"Type",
			"Createable",
			"Updateable",
			"Required",
			"DefaultValue",
			"HelpText",
			"Options",
			"ProjectionConfig",
			"ReferenceTo",
			"WritableReferenceTo",
			"DeleteRule",
			"Validator",
			"ValidatorConfig",
			"ControllingField",
			"ChildRelationshipName",
			"SiblingMaxParentField",
			"WriteProtectionPolicy",
			"ReadProtectionPolicy",
			"IsUnique",
			"TrackHistory",
			"IsCustomized",
		}).AddRow(
			"Widget",
			"ExternalRemoteId__c",
			"string",
			true,
			true,
			false,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			liveUnique,
			false,
			isCustomized,
		))
}

func assertDiffContains(t *testing.T, diffs []FieldDiff, want string) {
	t.Helper()
	for _, diff := range diffs {
		if strings.Contains(diff.String(), want) {
			return
		}
	}
	t.Fatalf("diffs %#v do not contain %q", diffs, want)
}

func boolPtr(v bool) *bool {
	return &v
}
