package packs

import (
	"reflect"
	"strings"
	"testing"
)

const connectorPushContractPayloadJSON = `{
	"flowDeveloperName": "Contact_To_Remote",
	"flowActionName": "PushContact",
	"slotLineageId": "contact-remote-sync",
	"sourceObject": "Contact",
	"destinationSlotId": "remote-contact",
	"profileOwnerPack": "hubspot-connector",
	"profileName": "default",
	"targetObject": "remoteContact",
	"createPathTemplate": "/crm/v3/objects/contacts",
	"updatePathTemplate": "/crm/v3/objects/contacts/{remoteId}",
	"responseIdPath": "id",
	"remoteIdField": "HubSpotContactId__c",
	"remoteIdProvenanceField": "HubSpotContactIdSource__c"
}`

const connectorPushOutboundMappingPayloadJSON = `[
	{
		"sourceObject": "Contact",
		"targetObject": "remoteContact",
		"actionType": "PushToConnector",
		"direction": "outbound",
		"mappings": [
			{"sourceField": "Email", "targetField": "email"}
		]
	}
]`

const connectorPushStandardFlowPayloadJSON = `{
	"start": {"object": "Contact"},
	"actionCalls": [
		{
			"name": "PushContact",
			"actionType": "PushToConnector"
		}
	]
}`

func connectorPushStandardFlowManifestComponentJSON() string {
	return `{
			"kind": "standard_flow",
			"businessKey": "Contact_To_Remote",
			"manageability": "defaults_only",
			"payload": ` + jsonRawMessage(connectorPushStandardFlowPayloadJSON) + `
		}`
}

func TestConnectorPushContractKind_AppPackAcceptedWithPackOwnedMapping(t *testing.T) {
	root := writePackFixture(t, "jira-like-sample", validPackManifest(`[
		`+connectorPushStandardFlowManifestComponentJSON()+`,
		{
			"kind": "connector_field_mapping",
			"businessKey": "jira-like-sample/contact-push-outbound",
			"manageability": "locked",
			"payload": `+jsonRawMessage(connectorPushOutboundMappingPayloadJSON)+`
		},
		{
			"kind": "connector_push_contract",
			"businessKey": "jira-like-sample/Contact_To_Remote/PushContact/Contact/remote-contact",
			"manageability": "locked",
			"payload": `+jsonRawMessage(connectorPushContractPayloadJSON)+`
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	if report.HasErrors() {
		t.Fatalf("connector_push_contract app-pack should validate, got:\n%s", report.Error())
	}
}

func TestConnectorPushContractKind_ConnectorPackRejected(t *testing.T) {
	root := writePackFixture(t, "hubspot-connector", validConnectorPackManifest("hubspot-connector", `[
		{
			"kind": "connector_push_contract",
			"businessKey": "hubspot-connector/Contact_To_Remote/PushContact/Contact/remote-contact",
			"manageability": "locked",
			"payload": `+jsonRawMessage(connectorPushContractPayloadJSON)+`
		}
	]`))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	assertInstallableDiagnostic(t, report, "manifest.component.unsupported_for_category")
}

func TestConnectorPushContractValidatePackRejectsDynamicWriteTarget(t *testing.T) {
	dynamicPayload := strings.Replace(connectorPushContractPayloadJSON, `"remoteIdField": "HubSpotContactId__c"`, `"remoteIdField": {"elementReference": "$Record.RemoteIdField__c"}`, 1)
	root := writePackFixture(t, "jira-like-sample", validPackManifest(`[
		{
			"kind": "connector_push_contract",
			"businessKey": "jira-like-sample/Contact_To_Remote/PushContact/Contact/remote-contact",
			"manageability": "locked",
			"payload": `+jsonRawMessage(dynamicPayload)+`
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	assertInstallableDiagnostic(t, report, "component.payload.invalid_schema")
	if !strings.Contains(report.Error(), "remoteIdField must be a pack-owned literal string") {
		t.Fatalf("expected literal writeback field diagnostic, got:\n%s", report.Error())
	}
}

func TestConnectorPushContractValidatePackRejectsBusinessKeyMismatch(t *testing.T) {
	root := writePackFixture(t, "jira-like-sample", validPackManifest(`[
		`+connectorPushStandardFlowManifestComponentJSON()+`,
		{
			"kind": "connector_push_contract",
			"businessKey": "other-pack/Contact_To_Remote/PushContact/Contact/remote-contact",
			"manageability": "locked",
			"payload": `+jsonRawMessage(connectorPushContractPayloadJSON)+`
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	assertInstallableDiagnostic(t, report, "connector_push_contract.business_key.mismatch_payload")
}

func TestConnectorPushContractValidatePackRequiresOwnedOutboundMapping(t *testing.T) {
	root := writePackFixture(t, "jira-like-sample", validPackManifest(`[
		`+connectorPushStandardFlowManifestComponentJSON()+`,
		{
			"kind": "connector_push_contract",
			"businessKey": "jira-like-sample/Contact_To_Remote/PushContact/Contact/remote-contact",
			"manageability": "locked",
			"payload": `+jsonRawMessage(connectorPushContractPayloadJSON)+`
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	assertInstallableDiagnostic(t, report, "component.plan.invalid")
	if !strings.Contains(report.Error(), "requires an outbound connector_field_mapping owned by jira-like-sample") {
		t.Fatalf("expected mapping ownership diagnostic, got:\n%s", report.Error())
	}
}

func TestNewSourceBackedConnectorPushContractComponent_AssemblesProjectionFromPackOwner(t *testing.T) {
	component := ManifestComponent{
		Kind:          ComponentKindConnectorPushContract,
		BusinessKey:   "jira-like-sample/Contact_To_Remote/PushContact/Contact/remote-contact",
		Manageability: ManageabilityLocked,
		Payload:       []byte(connectorPushContractPayloadJSON),
	}
	comp, contract, err := newSourceBackedConnectorPushContractComponent("jira-like-sample", component, []byte(connectorPushContractPayloadJSON), connectorPushTestFlowPayloads(connectorPushStandardFlowPayloadJSON))
	if err != nil {
		t.Fatalf("newSourceBackedConnectorPushContractComponent: %v", err)
	}
	if contract.ConnectorOwnerKey != "jira-like-sample" {
		t.Fatalf("ConnectorOwnerKey = %q, want pack owner", contract.ConnectorOwnerKey)
	}
	if contract.ActionSlotID != "contact-remote-sync" {
		t.Fatalf("ActionSlotID = %q, want stable slot lineage literal", contract.ActionSlotID)
	}
	if contract.SameOriginWritePolicy != "suppress" {
		t.Fatalf("SameOriginWritePolicy = %q, want suppress default", contract.SameOriginWritePolicy)
	}
	projection, err := comp.ExpectedProjection()
	if err != nil {
		t.Fatalf("ExpectedProjection: %v", err)
	}
	if !reflect.DeepEqual(projection, contract) {
		t.Fatalf("ExpectedProjection = %#v, want assembled contract %#v", projection, contract)
	}
}

func TestNewSourceBackedConnectorPushContractComponent_AllowsSameOriginPatchExistingPolicy(t *testing.T) {
	payload := strings.Replace(connectorPushContractPayloadJSON, `"destinationSlotId": "remote-contact",`, `"destinationSlotId": "remote-contact",`+"\n\t"+`"sameOriginWritePolicy": "allowPatchExisting",`, 1)
	component := ManifestComponent{
		Kind:          ComponentKindConnectorPushContract,
		BusinessKey:   "jira-like-sample/Contact_To_Remote/PushContact/Contact/remote-contact",
		Manageability: ManageabilityLocked,
		Payload:       []byte(payload),
	}
	_, contract, err := newSourceBackedConnectorPushContractComponent("jira-like-sample", component, []byte(payload), connectorPushTestFlowPayloads(connectorPushStandardFlowPayloadJSON))
	if err != nil {
		t.Fatalf("newSourceBackedConnectorPushContractComponent: %v", err)
	}
	if contract.SameOriginWritePolicy != "allowPatchExisting" {
		t.Fatalf("SameOriginWritePolicy = %q, want allowPatchExisting", contract.SameOriginWritePolicy)
	}
}

func TestConnectorPushContractValidatePackRejectsInvalidSameOriginWritePolicy(t *testing.T) {
	payload := strings.Replace(connectorPushContractPayloadJSON, `"destinationSlotId": "remote-contact",`, `"destinationSlotId": "remote-contact",`+"\n\t"+`"sameOriginWritePolicy": "createToo",`, 1)
	root := writePackFixture(t, "jira-like-sample", validPackManifest(`[
		`+connectorPushStandardFlowManifestComponentJSON()+`,
		{
			"kind": "connector_field_mapping",
			"businessKey": "jira-like-sample/contact-push-outbound",
			"manageability": "locked",
			"payload": `+jsonRawMessage(connectorPushOutboundMappingPayloadJSON)+`
		},
		{
			"kind": "connector_push_contract",
			"businessKey": "jira-like-sample/Contact_To_Remote/PushContact/Contact/remote-contact",
			"manageability": "locked",
			"payload": `+jsonRawMessage(payload)+`
		}
	]`, ``))

	report := ValidateInstallablePackSourceWithOptions("local_path:"+root, testPackSourceOptions(t))
	assertInstallableDiagnostic(t, report, "component.payload.invalid_schema")
	if !strings.Contains(report.Error(), "sameOriginWritePolicy must be") {
		t.Fatalf("expected sameOriginWritePolicy diagnostic, got:\n%s", report.Error())
	}
}

func TestNewSourceBackedConnectorPushContractComponent_BindsActualStandardFlowSlot(t *testing.T) {
	component := ManifestComponent{
		Kind:          ComponentKindConnectorPushContract,
		BusinessKey:   "jira-like-sample/Contact_To_Remote/PushContact/Contact/remote-contact",
		Manageability: ManageabilityLocked,
		Payload:       []byte(connectorPushContractPayloadJSON),
	}
	cases := []struct {
		name     string
		flows    map[string][]byte
		wantText string
	}{
		{"missing flow", nil, "references standard_flow"},
		{"flow casing mismatch", map[string][]byte{"contact_to_remote.flow.json": []byte(connectorPushStandardFlowPayloadJSON)}, "references standard_flow"},
		{"missing action", connectorPushTestFlowPayloads(strings.Replace(connectorPushStandardFlowPayloadJSON, `"name": "PushContact"`, `"name": "OtherAction"`, 1)), "action that is not declared"},
		{"action casing mismatch", connectorPushTestFlowPayloads(strings.Replace(connectorPushStandardFlowPayloadJSON, `"name": "PushContact"`, `"name": "pushcontact"`, 1)), "action that is not declared"},
		{"wrong action type", connectorPushTestFlowPayloads(strings.Replace(connectorPushStandardFlowPayloadJSON, `"actionType": "PushToConnector"`, `"actionType": "emailAlert"`, 1)), "is not PushToConnector"},
		{"source object mismatch", connectorPushTestFlowPayloads(strings.Replace(connectorPushStandardFlowPayloadJSON, `"object": "Contact"`, `"object": "Lead"`, 1)), "does not match standard_flow start object"},
		{"source object casing mismatch", connectorPushTestFlowPayloads(strings.Replace(connectorPushStandardFlowPayloadJSON, `"object": "Contact"`, `"object": "contact"`, 1)), "does not match standard_flow start object"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, _, err := newSourceBackedConnectorPushContractComponent("jira-like-sample", component, []byte(connectorPushContractPayloadJSON), tc.flows)
			if err == nil {
				t.Fatal("expected flow-slot binding error")
			}
			if !strings.Contains(err.Error(), tc.wantText) {
				t.Fatalf("error = %q, want substring %q", err.Error(), tc.wantText)
			}
		})
	}
}

func connectorPushTestFlowPayloads(raw string) map[string][]byte {
	return map[string][]byte{"Contact_To_Remote.flow.json": []byte(raw)}
}
