package credentialgeneration

import (
	"bytes"
	"encoding/hex"
	"errors"
	"strings"
	"testing"
	"unicode/utf8"
)

func TestReceiptProtocolResolutionIsExact(t *testing.T) {
	for _, resolve := range []struct {
		name string
		fn   func(ReceiptProtocolVersion) (ReceiptProtocol, error)
	}{
		{name: "authoring", fn: ResolveReceiptProtocolForAuthoring},
		{name: "retained read", fn: ResolveReceiptProtocolForRead},
	} {
		t.Run(resolve.name, func(t *testing.T) {
			protocol, err := resolve.fn(ReceiptProtocolV1)
			if err != nil || protocol.Version() != ReceiptProtocolV1 {
				t.Fatalf("resolve v1 = %q, %v", protocol.Version(), err)
			}
			for _, unsupported := range []ReceiptProtocolVersion{
				"",
				"NEXUSCRM.CONNECTOR-CRED-GEN.V1",
				"nexuscrm.connector-cred-gen.v1 ",
				"connector-cred-gen.v1",
				"nexuscrm.connector-cred-gen.v2",
			} {
				if _, err := resolve.fn(unsupported); !errors.Is(err, ErrUnsupportedReceiptProtocol) {
					t.Fatalf("resolve(%q) error = %v, want ErrUnsupportedReceiptProtocol", unsupported, err)
				}
			}
		})
	}

	input := credentialGenerationTestInput("generation-explicit-protocol")
	input.ReceiptProtocolVersion = ""
	if _, err := NewGeneration(input); !errors.Is(err, ErrUnsupportedReceiptProtocol) {
		t.Fatalf("NewGeneration(empty protocol) error = %v", err)
	}
}

func TestCredentialGenerationReceiptV1Golden(t *testing.T) {
	input := AuthoringInput{
		ReceiptProtocolVersion:   ReceiptProtocolV1,
		ID:                       "generation-A",
		StableCredentialIdentity: "credential-sync",
		ProviderScopeID:          "provider-scope-01",
		ConnectorAccountID:       "account-01",
		SecretBackendProtocol:    "vault-kv-v2",
		SecretBackendVersionRef:  "secret/version/0007",
		TransportConfiguration:   []byte{0x00, 'h', 't', 't', 'p', 0xff, '\n'},
	}
	generation := mustCredentialGeneration(t, input)

	const wantConfigurationDigest = "sha256:70918617b431c49e025863ffb077ee3ce3640fac9cb9e58608f27462b639eaf1"
	const wantReceiptHex = "6e6578757363726d2e636f6e6e6563746f722d63726564656e7469616c2d67656e65726174696f6e2d6f776e65722d726563656970742e763100000000000000001e6e6578757363726d2e636f6e6e6563746f722d637265642d67656e2e7631000000000000000c67656e65726174696f6e2d41000000000000000f63726564656e7469616c2d73796e63000000000000001170726f76696465722d73636f70652d3031000000000000000a6163636f756e742d3031000000000000000b7661756c742d6b762d763200000000000000137365637265742f76657273696f6e2f3030303700000000000000070068747470ff0a00000000000000477368613235363a37303931383631376234333163343965303235383633666662303737656533636533363430666163396362396535383630386632373436326236333965616631"
	const wantReceiptDigest = "sha256:8709f1ef8edce5fce5f5d29c1f5d1cbf59f8e6597286f065cd1503c11a5507b0"
	if generation.TransportConfigurationDigest() != wantConfigurationDigest {
		t.Fatalf("configuration digest = %q, want %q", generation.TransportConfigurationDigest(), wantConfigurationDigest)
	}
	if got := hex.EncodeToString(generation.OwnerReceipt()); got != wantReceiptHex {
		t.Fatalf("receipt hex = %q, want %q", got, wantReceiptHex)
	}
	if generation.OwnerReceiptDigest() != wantReceiptDigest {
		t.Fatalf("receipt digest = %q, want %q", generation.OwnerReceiptDigest(), wantReceiptDigest)
	}
}

func TestCredentialGenerationReceiptBindsEverySemanticAxis(t *testing.T) {
	baseInput := credentialGenerationTestInput("generation-semantic-axes")
	base := mustCredentialGeneration(t, baseInput)
	tests := []struct {
		name   string
		mutate func(*AuthoringInput)
	}{
		{name: "generation ID", mutate: func(input *AuthoringInput) { input.ID += "-other" }},
		{name: "stable identity", mutate: func(input *AuthoringInput) { input.StableCredentialIdentity += "-other" }},
		{name: "provider scope", mutate: func(input *AuthoringInput) { input.ProviderScopeID += "-other" }},
		{name: "connector account", mutate: func(input *AuthoringInput) { input.ConnectorAccountID += "-other" }},
		{name: "backend protocol", mutate: func(input *AuthoringInput) { input.SecretBackendProtocol += "-other" }},
		{name: "backend version", mutate: func(input *AuthoringInput) { input.SecretBackendVersionRef += "-other" }},
		{name: "transport bytes", mutate: func(input *AuthoringInput) { input.TransportConfiguration = append(input.TransportConfiguration, 0) }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			input := baseInput
			input.TransportConfiguration = append([]byte(nil), baseInput.TransportConfiguration...)
			test.mutate(&input)
			changed := mustCredentialGeneration(t, input)
			if bytes.Equal(changed.OwnerReceipt(), base.OwnerReceipt()) ||
				changed.OwnerReceiptDigest() == base.OwnerReceiptDigest() {
				t.Fatalf("%s did not change owner receipt identity", test.name)
			}
		})
	}
}

func TestCredentialGenerationCanonicalFramingAndBinaryExactness(t *testing.T) {
	first := credentialGenerationTestInput("generation-framing-a")
	first.StableCredentialIdentity = "a"
	first.ProviderScopeID = "bc"
	second := credentialGenerationTestInput("generation-framing-a")
	second.StableCredentialIdentity = "ab"
	second.ProviderScopeID = "c"
	if bytes.Equal(
		mustCredentialGeneration(t, first).OwnerReceipt(),
		mustCredentialGeneration(t, second).OwnerReceipt(),
	) {
		t.Fatal("length framing allowed a structural collision")
	}

	composed := credentialGenerationTestInput("generation-unicode")
	composed.StableCredentialIdentity = "caf\u00e9"
	decomposed := credentialGenerationTestInput("generation-unicode")
	decomposed.StableCredentialIdentity = "cafe\u0301"
	if bytes.Equal(
		mustCredentialGeneration(t, composed).OwnerReceipt(),
		mustCredentialGeneration(t, decomposed).OwnerReceipt(),
	) {
		t.Fatal("Unicode identity was normalized")
	}

	binaryA := credentialGenerationTestInput("generation-binary")
	binaryA.TransportConfiguration = []byte{0x00, 0xff, 'A', ' '}
	binaryB := binaryA
	binaryB.TransportConfiguration = []byte{0x00, 0xff, 'a', ' '}
	firstBinary := mustCredentialGeneration(t, binaryA)
	secondBinary := mustCredentialGeneration(t, binaryB)
	if firstBinary.TransportConfigurationDigest() == secondBinary.TransportConfigurationDigest() {
		t.Fatal("binary transport configuration was normalized")
	}
}

func TestCredentialGenerationValidationAndBounds(t *testing.T) {
	invalidUTF8 := string([]byte{0xff})
	tests := []struct {
		name  string
		value string
	}{
		{name: "empty", value: ""},
		{name: "leading whitespace", value: " value"},
		{name: "trailing whitespace", value: "value "},
		{name: "nul", value: "value\x00other"},
		{name: "control", value: "value\nother"},
		{name: "invalid utf8", value: invalidUTF8},
		{name: "too long", value: strings.Repeat("x", maxIdentifierBytes+1)},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			input := credentialGenerationTestInput("generation-invalid")
			input.SecretBackendVersionRef = test.value
			if _, err := NewGeneration(input); !errors.Is(err, ErrInvalidGeneration) {
				t.Fatalf("NewGeneration() error = %v, want ErrInvalidGeneration", err)
			}
		})
	}
	if !utf8.ValidString(strings.Repeat("x", maxIdentifierBytes)) {
		t.Fatal("test fixture is not valid UTF-8")
	}
	valid := credentialGenerationTestInput("generation-at-bounds")
	valid.ID = strings.Repeat("x", maxIdentifierBytes)
	valid.TransportConfiguration = make([]byte, MaxTransportConfigurationBytes)
	if _, err := NewGeneration(valid); err != nil {
		t.Fatalf("NewGeneration(at bounds): %v", err)
	}
	invalid := valid
	invalid.TransportConfiguration = make([]byte, MaxTransportConfigurationBytes+1)
	if _, err := NewGeneration(invalid); !errors.Is(err, ErrInvalidGeneration) {
		t.Fatalf("NewGeneration(over transport bound) error = %v", err)
	}
}

func TestCredentialGenerationDefensivelyCopiesBinaryFields(t *testing.T) {
	input := credentialGenerationTestInput("generation-defensive-copy")
	generation := mustCredentialGeneration(t, input)
	wantConfiguration := generation.TransportConfiguration()
	wantReceipt := generation.OwnerReceipt()

	input.TransportConfiguration[0] ^= 0xff
	configuration := generation.TransportConfiguration()
	receipt := generation.OwnerReceipt()
	configuration[0] ^= 0xff
	receipt[0] ^= 0xff
	if !bytes.Equal(generation.TransportConfiguration(), wantConfiguration) {
		t.Fatal("transport configuration retained caller or getter storage")
	}
	if !bytes.Equal(generation.OwnerReceipt(), wantReceipt) {
		t.Fatal("owner receipt retained getter storage")
	}

	reference := generation.SecretBackendReference()
	if reference.Protocol() != generation.SecretBackendProtocol() ||
		reference.VersionRef() != generation.SecretBackendVersionRef() {
		t.Fatal("secret backend reference did not preserve exact coordinates")
	}
}

func mustCredentialGeneration(t *testing.T, input AuthoringInput) Generation {
	t.Helper()
	generation, err := NewGeneration(input)
	if err != nil {
		t.Fatalf("NewGeneration: %v", err)
	}
	return generation
}
