package packs

import (
	"encoding/json"
	"reflect"
	"slices"
	"strings"
	"testing"

	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

func TestLoadPersistedConnectorPushContractSelectsExactProjection(t *testing.T) {
	row := testPersistedConnectorPushContractRow(t)
	mappingProjection, err := marshalRuntimeStartupShallowProjection([]connector.ConnectorFieldMappingInput{{
		Source:       "connector-a",
		SourceObject: "Contact",
		TargetObject: "remoteContact",
		ActionType:   "PushToConnector",
		SourceField:  "Email",
		TargetField:  "email",
		Direction:    connector.DirectionOutbound,
	}})
	if err != nil {
		t.Fatal(err)
	}
	raw, err := marshalRuntimeStartupShallowDeclaredRows([]runtimeStartupShallowDeclaredRow{
		{Kind: ComponentKindPermission, BusinessKey: "ViewSetup", Projection: `{"apiName":"ViewSetup"}`},
		{Kind: ComponentKindConnectorFieldMapping, BusinessKey: "connector-a/contact-push-outbound", Projection: mappingProjection},
		row,
	})
	if err != nil {
		t.Fatal(err)
	}
	got, err := LoadPersistedConnectorPushContract(raw, row.BusinessKey)
	if err != nil {
		t.Fatal(err)
	}
	if got.ConnectorOwnerKey != "connector-a" || got.ActionSlotID != "contact-remote-sync" ||
		got.RemoteIDField != "RemoteId" {
		t.Fatalf("persisted connector contract = %#v", got)
	}
}

func TestLoadPersistedConnectorPushContractReadsV2EnvelopeAsStableV1(t *testing.T) {
	row := testPersistedConnectorPushContractRow(t)
	var contract connector.ConnectorPushDurableMetadataContract
	if err := json.Unmarshal([]byte(row.Projection), &contract); err != nil {
		t.Fatal(err)
	}
	projection, err := marshalRuntimeStartupShallowProjection(
		persistedConnectorPushContractV2Envelope{
			FormatVersion:     persistedConnectorPushContractV2FormatVersion,
			Contract:          contract,
			MatchBeforeCreate: json.RawMessage(`{"Identity":{"SourceField":"Email"}}`),
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	row.Projection = projection
	mappingProjection, err := marshalRuntimeStartupShallowProjection([]connector.ConnectorFieldMappingInput{{
		Source:       "connector-a",
		SourceObject: "Contact",
		TargetObject: "remoteContact",
		ActionType:   "PushToConnector",
		SourceField:  "Email",
		TargetField:  "email",
		Direction:    connector.DirectionOutbound,
	}})
	if err != nil {
		t.Fatal(err)
	}
	raw, err := marshalRuntimeStartupShallowDeclaredRows([]runtimeStartupShallowDeclaredRow{
		{
			Kind:        ComponentKindConnectorFieldMapping,
			BusinessKey: "connector-a/contact-push-outbound",
			Projection:  mappingProjection,
		},
		row,
	})
	if err != nil {
		t.Fatal(err)
	}
	got, err := LoadPersistedConnectorPushContract(raw, row.BusinessKey)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(got, contract) {
		t.Fatalf("persisted V2 connector contract = %#v, want stable V1 %#v", got, contract)
	}
	normalized, err := normalizePersistedConnectorPushContractProjection(row)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(normalized, "FormatVersion") || strings.Contains(normalized, "MatchBeforeCreate") {
		t.Fatalf("normalized rollback projection retained V2 activation: %s", normalized)
	}
}

func TestLoadPersistedConnectorPushContractRejectsUnknownEnvelopeVersion(t *testing.T) {
	row := testPersistedConnectorPushContractRow(t)
	var contract connector.ConnectorPushDurableMetadataContract
	if err := json.Unmarshal([]byte(row.Projection), &contract); err != nil {
		t.Fatal(err)
	}
	projection, err := marshalRuntimeStartupShallowProjection(
		persistedConnectorPushContractV2Envelope{FormatVersion: 3, Contract: contract},
	)
	if err != nil {
		t.Fatal(err)
	}
	row.Projection = projection
	raw, err := marshalRuntimeStartupShallowDeclaredRows([]runtimeStartupShallowDeclaredRow{row})
	if err != nil {
		t.Fatal(err)
	}
	_, err = LoadPersistedConnectorPushContract(raw, row.BusinessKey)
	if err == nil || !strings.Contains(err.Error(), "unsupported connector push contract format version 3") {
		t.Fatalf("unknown persisted connector projection error = %v", err)
	}
}

func TestVerifyPersistedRuntimeDeclarationAgainstPackAcceptsV2ConnectorPushResidue(t *testing.T) {
	pack, err := newHistoricalReconciliationTestPack(
		t,
		connectorPushContractPayloadJSON,
		connectorPushOutboundMappingPayloadJSON,
	)
	if err != nil {
		t.Fatal(err)
	}
	expected, err := BuildPersistedRuntimeDeclaration(pack)
	if err != nil {
		t.Fatal(err)
	}
	rows, err := unmarshalRuntimeStartupShallowDeclaredRows(expected.StartupShallowDeclaredRowsJSON())
	if err != nil {
		t.Fatal(err)
	}
	found := false
	for index := range rows {
		if rows[index].Kind != ComponentKindConnectorPushContract {
			continue
		}
		var contract connector.ConnectorPushDurableMetadataContract
		if err := json.Unmarshal([]byte(rows[index].Projection), &contract); err != nil {
			t.Fatal(err)
		}
		rows[index].Projection, err = marshalRuntimeStartupShallowProjection(
			persistedConnectorPushContractV2Envelope{
				FormatVersion:     persistedConnectorPushContractV2FormatVersion,
				Contract:          contract,
				MatchBeforeCreate: json.RawMessage(`{"Identity":{"SourceField":"Email"}}`),
			},
		)
		if err != nil {
			t.Fatal(err)
		}
		found = true
	}
	if !found {
		t.Fatal("fixture has no connector push contract row")
	}
	startupShallowJSON, err := marshalRuntimeStartupShallowDeclaredRows(rows)
	if err != nil {
		t.Fatal(err)
	}
	persistedRows, err := unmarshalRuntimeStartupShallowDeclaredRows(startupShallowJSON)
	if err != nil {
		t.Fatal(err)
	}
	retainedV2 := false
	for _, row := range persistedRows {
		retainedV2 = retainedV2 || strings.Contains(row.Projection, `"FormatVersion":2`)
	}
	if !retainedV2 {
		t.Fatalf("test fixture lost V2 envelope: %s", startupShallowJSON)
	}
	if err := VerifyPersistedRuntimeDeclarationAgainstPackWithSearchAllDefaultFieldsAndFormulaFunctionBundles(
		pack.Name(),
		expected.ObjectPayloadsJSON(),
		expected.RelatedListRulesJSON(),
		expected.SearchScopeOrderJSON(),
		expected.SearchAllDefaultFieldsJSON(),
		expected.FormulaFunctionBundlesJSON(),
		startupShallowJSON,
		pack,
	); err != nil {
		t.Fatalf("verify V2 connector push residue against stable V1 source: %v", err)
	}
}

func TestPersistedConnectorPushRecoveryEligibilityIsSeparateImmutableSidecar(t *testing.T) {
	withoutSidecar, err := newHistoricalReconciliationTestPack(
		t,
		connectorPushContractPayloadJSON,
		connectorPushOutboundMappingPayloadJSON,
	)
	if err != nil {
		t.Fatalf("build old-generation pack: %v", err)
	}
	payloadWithSidecar := strings.Replace(
		connectorPushContractPayloadJSON,
		`"sourceObject": "Contact",`,
		`"sourceObject": "Contact", "recoverySourceEligibility": {"requiredFalseBooleanFields": ["Archived"]},`,
		1,
	)
	withSidecar, err := newHistoricalReconciliationTestPack(
		t,
		payloadWithSidecar,
		connectorPushOutboundMappingPayloadJSON,
	)
	if err != nil {
		t.Fatalf("build replacement-generation pack: %v", err)
	}
	oldDeclaration, err := BuildPersistedRuntimeDeclaration(withoutSidecar)
	if err != nil {
		t.Fatal(err)
	}
	newDeclaration, err := BuildPersistedRuntimeDeclaration(withSidecar)
	if err != nil {
		t.Fatal(err)
	}
	businessKey := "jira-like-sample/Contact_To_Remote/PushContact/Contact/remote-contact"
	oldContract, err := LoadPersistedConnectorPushContract(oldDeclaration.StartupShallowDeclaredRowsJSON(), businessKey)
	if err != nil {
		t.Fatal(err)
	}
	newContract, err := LoadPersistedConnectorPushContract(newDeclaration.StartupShallowDeclaredRowsJSON(), businessKey)
	if err != nil {
		t.Fatal(err)
	}
	oldProjection, err := marshalRuntimeStartupShallowProjection(oldContract)
	if err != nil {
		t.Fatal(err)
	}
	newProjection, err := marshalRuntimeStartupShallowProjection(newContract)
	if err != nil {
		t.Fatal(err)
	}
	if newProjection != oldProjection {
		t.Fatalf("durable connector contract changed with recovery sidecar:\nold: %s\nnew: %s", oldProjection, newProjection)
	}
	oldEligibility, err := LoadPersistedConnectorPushRecoverySourceEligibility(
		oldDeclaration.StartupShallowDeclaredRowsJSON(),
		businessKey,
	)
	if err != nil {
		t.Fatal(err)
	}
	if oldEligibility != nil {
		t.Fatalf("old generation recovery eligibility = %#v, want nil", oldEligibility)
	}
	newEligibility, err := LoadPersistedConnectorPushRecoverySourceEligibility(
		newDeclaration.StartupShallowDeclaredRowsJSON(),
		businessKey,
	)
	if err != nil {
		t.Fatal(err)
	}
	if newEligibility == nil || !slices.Equal(newEligibility.RequiredFalseBooleanFields, []string{"Archived"}) {
		t.Fatalf("replacement generation recovery eligibility = %#v", newEligibility)
	}
}

func TestLoadPersistedConnectorPushRecoverySourceEligibilityFailsClosed(t *testing.T) {
	businessKey := "connector-a/Contact_To_Remote/PushContact/Contact/remote-contact"
	tests := []struct {
		name       string
		projection string
		duplicate  bool
		want       string
	}{
		{name: "duplicate", projection: `{"RequiredFalseBooleanFields":["Archived"]}`, duplicate: true, want: "ambiguous"},
		{name: "unknown projection field", projection: `{"RequiredFalseBooleanFields":["Archived"],"Future":true}`, want: "unknown field"},
		{name: "empty field set", projection: `{"RequiredFalseBooleanFields":[]}`, want: "must not be empty"},
		{name: "case-folded duplicate field", projection: `{"RequiredFalseBooleanFields":["Archived","archived"]}`, want: "duplicate"},
		{name: "dynamic field", projection: `{"RequiredFalseBooleanFields":["{!record.Archived}"]}`, want: "pack-owned literal field name"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			row := runtimeStartupShallowDeclaredRow{
				Kind:        componentKindConnectorPushRecoverySourceEligibility,
				BusinessKey: businessKey,
				Projection:  test.projection,
			}
			rows := []runtimeStartupShallowDeclaredRow{row}
			if test.duplicate {
				rows = append(rows, row)
			}
			raw, err := json.Marshal(rows)
			if err != nil {
				t.Fatal(err)
			}
			_, err = LoadPersistedConnectorPushRecoverySourceEligibility(string(raw), businessKey)
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("LoadPersistedConnectorPushRecoverySourceEligibility error = %v, want %q", err, test.want)
			}
		})
	}
}

func TestLoadPersistedConnectorPushContractFailsClosed(t *testing.T) {
	valid := testPersistedConnectorPushContractRow(t)
	validJSON := mustMarshalConnectorRuntimeRow(t, valid)
	unknown := valid
	unknown.Projection = strings.Replace(unknown.Projection, `"RemoteIDField":"RemoteId"`, `"RemoteIDField":"RemoteId","unexpected":true`, 1)
	mixed := valid
	mixed.Projection = strings.Replace(mixed.Projection, `"FlowDeveloperName":"Contact_To_Remote"`, `"FlowDeveloperName":"Other_Flow"`, 1)
	tests := []struct {
		name string
		raw  string
		want string
	}{
		{name: "missing", raw: `[]`, want: "absent from persisted generation"},
		{name: "duplicate", raw: `[` + validJSON + `,` + validJSON + `]`, want: "ambiguous"},
		{name: "unknown projection field", raw: `[` + mustMarshalConnectorRuntimeRow(t, unknown) + `]`, want: "unknown field"},
		{name: "mixed identity", raw: `[` + mustMarshalConnectorRuntimeRow(t, mixed) + `]`, want: "does not match business key"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := LoadPersistedConnectorPushContract(test.raw, valid.BusinessKey)
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("LoadPersistedConnectorPushContract error = %v, want %q", err, test.want)
			}
		})
	}
}

func testPersistedConnectorPushContractRow(t *testing.T) runtimeStartupShallowDeclaredRow {
	t.Helper()
	contract := connector.ConnectorPushDurableMetadataContract{
		ConnectorOwnerKey: "connector-a",
		FlowDeveloperName: "Contact_To_Remote",
		FlowActionName:    "PushContact",
		ActionSlotID:      "contact-remote-sync",
		ActionType:        "PushToConnector",
		SourceObject:      "Contact",
		DestinationSlotID: "remote-contact",
		TargetObject:      "remoteContact",
		ProfileName:       "default",
		RemoteIDField:     "RemoteId",
	}
	projection, err := marshalRuntimeStartupShallowProjection(contract)
	if err != nil {
		t.Fatal(err)
	}
	return runtimeStartupShallowDeclaredRow{
		Kind:        ComponentKindConnectorPushContract,
		BusinessKey: "connector-a/Contact_To_Remote/PushContact/Contact/remote-contact",
		Projection:  projection,
	}
}

func mustRuntimeStartupShallowJSON(t *testing.T, rows []runtimeStartupShallowDeclaredRow) string {
	t.Helper()
	raw, err := marshalRuntimeStartupShallowDeclaredRows(rows)
	if err != nil {
		t.Fatal(err)
	}
	return raw
}

func mustMarshalConnectorRuntimeRow(t *testing.T, row runtimeStartupShallowDeclaredRow) string {
	t.Helper()
	raw, err := json.Marshal(row)
	if err != nil {
		t.Fatal(err)
	}
	return string(raw)
}
