package serviceconfiguration

import (
	"bytes"
	"errors"
	"strings"
	"testing"

	"github.com/pingcap-inc/nexuscrm-connectors/features/connector/credentialgeneration"
	catalogv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
)

func TestDeriveGenerationNormalizesCompleteOwnerFact(t *testing.T) {
	input := serviceConfigurationTestInput("service-configuration-1")
	view := serviceConfigurationTestView()
	credentials := serviceConfigurationTestCredentials(t)

	generation, err := deriveGeneration(input, view, credentials)
	if err != nil {
		t.Fatal(err)
	}
	if generation.AccountCount() != 2 || generation.BindingCount() != 2 ||
		generation.CredentialSelectionCount() != 2 {
		t.Fatalf(
			"counts = %d/%d/%d, want 2/2/2",
			generation.AccountCount(),
			generation.BindingCount(),
			generation.CredentialSelectionCount(),
		)
	}
	accounts := generation.Accounts()
	if accounts[0].ConnectorAccountID() != "account-a" ||
		accounts[1].ConnectorAccountID() != "account-b" {
		t.Fatalf("account order = %q/%q", accounts[0].ConnectorAccountID(), accounts[1].ConnectorAccountID())
	}
	bindings := accounts[0].Bindings()
	if bindings[0].CatalogBindingOrdinal() != 0 || bindings[1].CatalogBindingOrdinal() != 1 {
		t.Fatalf(
			"binding order = %d/%d",
			bindings[0].CatalogBindingOrdinal(),
			bindings[1].CatalogBindingOrdinal(),
		)
	}
	if configuration := bindings[0].CanonicalConfiguration(); configuration == nil || len(configuration) != 0 {
		t.Fatalf("empty canonical configuration = %#v, want non-nil empty bytes", configuration)
	}
	selections := bindings[1].CredentialSelections()
	if selections[0].CredentialRef() != "credential-a" ||
		selections[1].CredentialRef() != "credential-b" {
		t.Fatalf(
			"credential order = %q/%q",
			selections[0].CredentialRef(),
			selections[1].CredentialRef(),
		)
	}
	if !validDigest(generation.GenerationDigest()) ||
		!validDigest(generation.OwnerReceiptDigest()) ||
		len(generation.OwnerReceipt()) == 0 ||
		len(generation.OwnerReceipt()) > MaxCanonicalReceiptBytes {
		t.Fatal("derived digest or compact receipt is invalid")
	}
	if generation.GenerationDigest() != "sha256:da21e007267d66a26f62d6c0e29d22eb844beec0a590107df3ac03c404dcead9" ||
		generation.OwnerReceiptDigest() != "sha256:33e110ca8e630852b22b91eb9b6ed8f920715237314014051eae964a0bf92a57" {
		t.Fatalf(
			"canonical vector = %s/%s",
			generation.GenerationDigest(),
			generation.OwnerReceiptDigest(),
		)
	}
	if err := generation.validateStored(); err != nil {
		t.Fatalf("validateStored() error = %v", err)
	}
}

func TestCanonicalGenerationIsOrderIndependentAndDefensive(t *testing.T) {
	input := serviceConfigurationTestInput("service-configuration-order")
	view := serviceConfigurationTestView()
	credentials := serviceConfigurationTestCredentials(t)
	first, err := deriveGeneration(input, view, credentials)
	if err != nil {
		t.Fatal(err)
	}

	input.Accounts[0], input.Accounts[1] = input.Accounts[1], input.Accounts[0]
	input.Accounts[0].Bindings[0], input.Accounts[0].Bindings[1] =
		input.Accounts[0].Bindings[1], input.Accounts[0].Bindings[0]
	input.Accounts[0].Bindings[1].CredentialSelections[0],
		input.Accounts[0].Bindings[1].CredentialSelections[1] =
		input.Accounts[0].Bindings[1].CredentialSelections[1],
		input.Accounts[0].Bindings[1].CredentialSelections[0]
	second, err := deriveGeneration(input, view, reverseCredentialGenerations(credentials))
	if err != nil {
		t.Fatal(err)
	}
	if !first.equal(second) {
		t.Fatal("permuted exhaustive sets changed the canonical generation")
	}

	input.Accounts[0].Bindings[1].CanonicalConfiguration[0] = 'X'
	input.Accounts[0].Bindings[1].CredentialSelections[0].CredentialRef = "changed"
	if bytes.HasPrefix(first.Accounts()[0].Bindings()[1].CanonicalConfiguration(), []byte("X")) ||
		first.Accounts()[0].Bindings()[1].CredentialSelections()[0].CredentialRef() == "changed" {
		t.Fatal("generation aliases caller-owned memory")
	}
	receipt := first.OwnerReceipt()
	receipt[0] ^= 0xff
	if bytes.Equal(receipt, first.OwnerReceipt()) {
		t.Fatal("OwnerReceipt aliases internal memory")
	}
}

func TestGenerationDigestCommitsFullCatalogAttestation(t *testing.T) {
	input := serviceConfigurationTestInput("service-configuration-attestation")
	view := serviceConfigurationTestView()
	credentials := serviceConfigurationTestCredentials(t)
	base, err := deriveGeneration(input, view, credentials)
	if err != nil {
		t.Fatal(err)
	}

	changed := view
	changed.Receipts.SecurityEpoch++
	if changed.Lineage.ViewDigest != view.Lineage.ViewDigest {
		t.Fatal("test setup unexpectedly changed Catalog view digest")
	}
	other, err := deriveGeneration(input, changed, credentials)
	if err != nil {
		t.Fatalf("security-epoch-only Catalog change rejected: %v", err)
	}
	if other.GenerationDigest() == base.GenerationDigest() ||
		bytes.Equal(other.OwnerReceipt(), base.OwnerReceipt()) {
		t.Fatal("Catalog receipt attestation change did not change generation identity")
	}

	changed = view
	changed.Lineage.CatalogRevision++
	changed.Receipts.CatalogRevision++
	changed.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(changed)
	other, err = deriveGeneration(input, changed, credentials)
	if err != nil {
		t.Fatal(err)
	}
	if other.GenerationDigest() == base.GenerationDigest() {
		t.Fatal("Catalog lineage change did not change generation digest")
	}
}

func TestDeriveGenerationRejectsStaleCatalogViewDigest(t *testing.T) {
	input := serviceConfigurationTestInput("service-configuration-stale-catalog-view")
	view := serviceConfigurationTestView()
	view.Bindings[0].DefinitionVersion++
	if view.Lineage.ViewDigest == catalogv1.ComputeIntegrationBindingViewDigest(view) {
		t.Fatal("test setup did not stale the Catalog view digest")
	}

	if _, err := deriveGeneration(
		input,
		view,
		serviceConfigurationTestCredentials(t),
	); !errors.Is(err, ErrInvalidGeneration) {
		t.Fatalf("deriveGeneration(stale Catalog digest) error = %v, want ErrInvalidGeneration", err)
	}
}

func TestDeriveGenerationRequiresExactCatalogAndCredentialClosure(t *testing.T) {
	view := serviceConfigurationTestView()
	credentials := serviceConfigurationTestCredentials(t)

	tests := []struct {
		name        string
		mutateInput func(*AuthoringInput)
		mutateView  func(*catalogv1.IntegrationBindingView)
		credentials func() []credentialgeneration.Generation
		want        error
	}{
		{
			name: "absent Catalog ordinal",
			mutateInput: func(input *AuthoringInput) {
				input.Accounts[1].Bindings[0].CatalogBindingOrdinal = 2
			},
			want: ErrInvalidGeneration,
		},
		{
			name: "missing Catalog CredentialRef",
			mutateInput: func(input *AuthoringInput) {
				input.Accounts[1].Bindings[0].CredentialSelections =
					input.Accounts[1].Bindings[0].CredentialSelections[:1]
			},
			want: ErrInvalidGeneration,
		},
		{
			name: "missing exact credential generation",
			credentials: func() []credentialgeneration.Generation {
				return credentials[:1]
			},
			want: ErrCredentialGenerationUnavailable,
		},
		{
			name: "unreferenced exact credential generation",
			credentials: func() []credentialgeneration.Generation {
				return append(
					append([]credentialgeneration.Generation(nil), credentials...),
					serviceConfigurationTestCredential(
						t,
						"credential-generation-extra",
						"credential-extra",
						"account-a",
						"provider-scope-a",
					),
				)
			},
			want: ErrInvalidGeneration,
		},
		{
			name: "credential account mismatch",
			credentials: func() []credentialgeneration.Generation {
				return []credentialgeneration.Generation{
					serviceConfigurationTestCredential(
						t,
						"credential-generation-a",
						"credential-a",
						"account-other",
						"provider-scope-a",
					),
					credentials[1],
				}
			},
			want: ErrInvalidGeneration,
		},
		{
			name: "Catalog receipt lineage mismatch",
			mutateView: func(value *catalogv1.IntegrationBindingView) {
				value.Receipts.SchemaGeneration++
			},
			want: ErrInvalidGeneration,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			input := serviceConfigurationTestInput("service-configuration-invalid")
			candidateView := view
			candidateCredentials := credentials
			if test.mutateInput != nil {
				test.mutateInput(&input)
			}
			if test.mutateView != nil {
				test.mutateView(&candidateView)
			}
			if test.credentials != nil {
				candidateCredentials = test.credentials()
			}
			if _, err := deriveGeneration(input, candidateView, candidateCredentials); !errors.Is(err, test.want) {
				t.Fatalf("deriveGeneration() error = %v, want %v", err, test.want)
			}
		})
	}
}

func TestAuthoringInputIsClosedAndBounded(t *testing.T) {
	view := serviceConfigurationTestView()
	credentials := serviceConfigurationTestCredentials(t)
	tests := []struct {
		name   string
		mutate func(*AuthoringInput)
	}{
		{name: "generation protocol omitted", mutate: func(input *AuthoringInput) {
			input.GenerationProtocolVersion = ""
		}},
		{name: "receipt protocol omitted", mutate: func(input *AuthoringInput) {
			input.ReceiptProtocolVersion = ""
		}},
		{name: "accounts absent", mutate: func(input *AuthoringInput) { input.Accounts = nil }},
		{name: "bindings absent", mutate: func(input *AuthoringInput) {
			input.Accounts[0].Bindings = nil
		}},
		{name: "selections absent", mutate: func(input *AuthoringInput) {
			input.Accounts[1].Bindings[0].CredentialSelections = nil
		}},
		{name: "duplicate account", mutate: func(input *AuthoringInput) {
			input.Accounts[1].ConnectorAccountID = input.Accounts[0].ConnectorAccountID
		}},
		{name: "duplicate binding", mutate: func(input *AuthoringInput) {
			input.Accounts[1].Bindings[1].CatalogBindingOrdinal =
				input.Accounts[1].Bindings[0].CatalogBindingOrdinal
		}},
		{name: "duplicate CredentialRef", mutate: func(input *AuthoringInput) {
			input.Accounts[1].Bindings[0].CredentialSelections[1].CredentialRef =
				input.Accounts[1].Bindings[0].CredentialSelections[0].CredentialRef
		}},
		{name: "oversized configuration", mutate: func(input *AuthoringInput) {
			input.Accounts[1].Bindings[0].CanonicalConfiguration =
				make([]byte, MaxCanonicalValueBytes+1)
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			input := serviceConfigurationTestInput("service-configuration-bounds")
			test.mutate(&input)
			if _, err := deriveGeneration(input, view, credentials); err == nil {
				t.Fatal("invalid authoring input accepted")
			}
		})
	}

	empty := serviceConfigurationTestInput("service-configuration-empty")
	empty.Accounts = []AccountInput{}
	if _, err := deriveGeneration(
		empty,
		view,
		[]credentialgeneration.Generation{},
	); err != nil {
		t.Fatalf("present empty account set rejected: %v", err)
	}
}

func TestStoredGenerationRejectsEveryDerivedFieldMismatch(t *testing.T) {
	generation, err := deriveGeneration(
		serviceConfigurationTestInput("service-configuration-stored"),
		serviceConfigurationTestView(),
		serviceConfigurationTestCredentials(t),
	)
	if err != nil {
		t.Fatal(err)
	}
	tests := []struct {
		name   string
		mutate func(*Generation)
	}{
		{name: "generation digest", mutate: func(value *Generation) {
			value.generationDigest = serviceConfigurationDigest("0")
		}},
		{name: "receipt", mutate: func(value *Generation) { value.ownerReceipt[0] ^= 0xff }},
		{name: "receipt digest", mutate: func(value *Generation) {
			value.ownerReceiptDigest = serviceConfigurationDigest("1")
		}},
		{name: "account count", mutate: func(value *Generation) { value.accountCount++ }},
		{name: "binding count", mutate: func(value *Generation) { value.bindingCount++ }},
		{name: "credential count", mutate: func(value *Generation) { value.credentialSelectionCount++ }},
		{name: "account row ID", mutate: func(value *Generation) { value.accounts[0].rowID = strings.Repeat("0", 64) }},
		{name: "binding row ID", mutate: func(value *Generation) { value.accounts[0].bindings[0].rowID = strings.Repeat("1", 64) }},
		{name: "selection ordinal", mutate: func(value *Generation) {
			value.accounts[0].bindings[1].credentialSelections[0].ordinal++
		}},
		{name: "security epoch", mutate: func(value *Generation) { value.catalog.receipts.SecurityEpoch++ }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			changed := generation.copy()
			test.mutate(&changed)
			if err := changed.validateStored(); !errors.Is(err, ErrStoredGenerationInvalid) {
				t.Fatalf("validateStored() error = %v, want ErrStoredGenerationInvalid", err)
			}
		})
	}
}

func TestProtocolResolutionIsExplicitAndReaderIndependent(t *testing.T) {
	if protocol, err := ResolveGenerationProtocolForAuthoring(GenerationProtocolV1); err != nil || protocol.Version() != GenerationProtocolV1 {
		t.Fatalf("ResolveGenerationProtocolForAuthoring(v1) = %#v, %v", protocol, err)
	}
	if _, err := ResolveGenerationProtocolForRead("v2"); !errors.Is(err, ErrUnsupportedGenerationProtocol) {
		t.Fatalf("ResolveGenerationProtocolForRead(v2) error = %v", err)
	}
	if protocol, err := ResolveReceiptProtocolForRead(ReceiptProtocolV1); err != nil || protocol.Version() != ReceiptProtocolV1 {
		t.Fatalf("ResolveReceiptProtocolForRead(v1) = %#v, %v", protocol, err)
	}
	if _, err := ResolveReceiptProtocolForAuthoring(""); !errors.Is(err, ErrUnsupportedReceiptProtocol) {
		t.Fatalf("ResolveReceiptProtocolForAuthoring(empty) error = %v", err)
	}
}

func serviceConfigurationTestInput(id string) AuthoringInput {
	return AuthoringInput{
		GenerationProtocolVersion: GenerationProtocolV1,
		ReceiptProtocolVersion:    ReceiptProtocolV1,
		ID:                        id,
		OwnerRevision:             serviceConfigurationRevision("configuration-owner-1", "1"),
		Accounts: []AccountInput{
			{
				ConnectorAccountID: "account-b",
				ProviderScopeID:    "provider-scope-b",
				AccountRevision:    serviceConfigurationRevision("account-owner-b", "2"),
				ProviderRevision:   serviceConfigurationRevision("provider-owner-b", "3"),
				Bindings:           []BindingInput{},
			},
			{
				ConnectorAccountID: "account-a",
				ProviderScopeID:    "provider-scope-a",
				AccountRevision:    serviceConfigurationRevision("account-owner-a", "4"),
				ProviderRevision:   serviceConfigurationRevision("provider-owner-a", "5"),
				Bindings: []BindingInput{
					{
						CatalogBindingOrdinal:      1,
						ConfigurationRevision:      serviceConfigurationRevision("route-owner-1", "6"),
						ConfigurationFormatVersion: "connector.configuration/v1",
						CanonicalConfiguration:     []byte(`{"route":"enabled"}`),
						CredentialSelections: []CredentialSelectionInput{
							{CredentialRef: "credential-b", CredentialGenerationID: "credential-generation-b"},
							{CredentialRef: "credential-a", CredentialGenerationID: "credential-generation-a"},
						},
					},
					{
						CatalogBindingOrdinal:      0,
						ConfigurationRevision:      serviceConfigurationRevision("mapping-owner-1", "7"),
						ConfigurationFormatVersion: "connector.configuration/v1",
						CanonicalConfiguration:     nil,
						CredentialSelections:       []CredentialSelectionInput{},
					},
				},
			},
		},
	}
}

func serviceConfigurationTestView() catalogv1.IntegrationBindingView {
	packDigest := "nexus-pack-sha256-v1:" + strings.Repeat("a", 64)
	view := catalogv1.IntegrationBindingView{
		OrgID:                "org-1",
		AppInstallationID:    "01900000-0000-7000-8000-000000000001",
		AuthorizedWorkloadID: "integration-1",
		Lineage: catalogv1.ViewLineage{
			CatalogRevision:      41,
			SchemaGeneration:     7,
			EngineGeneration:     9,
			SourceSnapshotDigest: serviceConfigurationDigest("b"),
		},
		Bindings: []catalogv1.IntegrationBinding{
			{
				Kind:               catalogv1.BindingConnectorFieldMapping,
				OwnerPack:          "connector-neutral",
				PackInstallationID: "pack-install-1",
				PackPayloadDigest:  packDigest,
				BusinessKey:        "mapping/default",
				DefinitionVersion:  1,
				CredentialRefs:     []string{},
			},
			{
				Kind:               catalogv1.BindingConnectorInboundRoute,
				OwnerPack:          "connector-neutral",
				PackInstallationID: "pack-install-1",
				PackPayloadDigest:  packDigest,
				BusinessKey:        "route/default",
				DefinitionVersion:  2,
				CredentialRefs:     []string{"credential-a", "credential-b"},
				RuntimeCommand: &catalogv1.RuntimeCommandBinding{
					CommandKind:          "runtime.flow.execute",
					PayloadFormatVersion: 1,
					DefinitionRef:        "flow-1",
					DefinitionVersion:    "1",
				},
			},
		},
		Receipts: catalogv1.Receipts{
			ProtocolVersion:   "1.0",
			CatalogRevision:   41,
			SchemaGeneration:  7,
			EngineGeneration:  9,
			SecurityEpoch:     11,
			CapabilityVersion: catalogv1.IntegrationBindingCapabilityName,
		},
	}
	view.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(view)
	return view
}

func serviceConfigurationTestCredentials(t *testing.T) []credentialgeneration.Generation {
	t.Helper()
	return []credentialgeneration.Generation{
		serviceConfigurationTestCredential(
			t,
			"credential-generation-a",
			"credential-a",
			"account-a",
			"provider-scope-a",
		),
		serviceConfigurationTestCredential(
			t,
			"credential-generation-b",
			"credential-b",
			"account-a",
			"provider-scope-a",
		),
	}
}

func serviceConfigurationTestCredential(
	t *testing.T,
	id string,
	stableIdentity string,
	accountID string,
	providerScopeID string,
) credentialgeneration.Generation {
	t.Helper()
	generation, err := credentialgeneration.NewGeneration(credentialgeneration.AuthoringInput{
		ReceiptProtocolVersion:   credentialgeneration.ReceiptProtocolV1,
		ID:                       id,
		StableCredentialIdentity: stableIdentity,
		ProviderScopeID:          providerScopeID,
		ConnectorAccountID:       accountID,
		SecretBackendProtocol:    "secret-backend/v1",
		SecretBackendVersionRef:  "secret-version-1",
		TransportConfiguration:   []byte{},
	})
	if err != nil {
		t.Fatalf("credentialgeneration.NewGeneration(%q): %v", id, err)
	}
	return generation
}

func reverseCredentialGenerations(
	values []credentialgeneration.Generation,
) []credentialgeneration.Generation {
	result := append([]credentialgeneration.Generation(nil), values...)
	for left, right := 0, len(result)-1; left < right; left, right = left+1, right-1 {
		result[left], result[right] = result[right], result[left]
	}
	return result
}

func serviceConfigurationRevision(id, hexCharacter string) ExactOwnerRevision {
	return ExactOwnerRevision{
		RevisionID:  id,
		ValueDigest: serviceConfigurationDigest(hexCharacter),
	}
}

func serviceConfigurationDigest(hexCharacter string) string {
	return "sha256:" + strings.Repeat(hexCharacter, 64)
}
