package catalogreconcile

import (
	"context"
	"encoding/base64"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/pingcap-inc/nexuscrm-controlplane/catalogapp"
)

func transitionConfigJSON() string {
	oldKey := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("o", 32)))
	newKey := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("n", 32)))
	return `{"schema":"` + PublisherTrustTransitionConfigSchemaV1 + `","operationId":"test2-sfcrm-036-publisher-key","publisherId":"io.nexuscrm","expectedOwnershipEpoch":"4","expectedActiveKeys":[{"keyId":"io.nexuscrm.sfcrm/0.35.0/old","algorithm":"ed25519","publicKeyBase64":"` + oldKey + `","validFrom":"2026-01-01T00:00:00Z","validUntil":"2036-01-01T00:00:00Z"}],"targetActiveKeys":[{"keyId":"io.nexuscrm.sfcrm/0.36.0/new","algorithm":"ed25519","publicKeyBase64":"` + newKey + `","validFrom":"2026-01-01T00:00:00Z","validUntil":"2036-01-01T00:00:00Z"}]}`
}

func registered(trust catalogapp.PublisherTrust, epoch uint64) catalogapp.RegisteredPublisher {
	result := catalogapp.RegisteredPublisher{PublisherID: trust.PublisherID, OwnershipEpoch: epoch, Active: trust.Active}
	for _, key := range trust.Keys {
		result.Keys = append(result.Keys, catalogapp.RegisteredPublisherKey{PublisherKey: key, Active: true})
	}
	return result
}

func TestParseConfigRejectsAmbiguousOrSelfAuthorizingInput(t *testing.T) {
	config, err := ParsePublisherTrustTransitionConfig([]byte(transitionConfigJSON()))
	if err != nil {
		t.Fatal(err)
	}
	if config.expectedEpoch != 4 || len(config.expectedTrust.Keys) != 1 || len(config.targetTrust.Keys) != 1 {
		t.Fatalf("parsed config = %#v", config)
	}
	for _, raw := range []string{
		strings.Replace(transitionConfigJSON(), `"operationId":`, `"operationId":"duplicate","operationId":`, 1),
		strings.Replace(transitionConfigJSON(), `"publisherId":`, `"unknown":true,"publisherId":`, 1),
		strings.Replace(transitionConfigJSON(), `"expectedOwnershipEpoch":"4"`, `"expectedOwnershipEpoch":"04"`, 1),
		strings.Replace(transitionConfigJSON(), `"targetActiveKeys":[{"keyId":"io.nexuscrm.sfcrm/0.36.0/new"`, `"targetActiveKeys":[]`, 1),
		strings.Replace(transitionConfigJSON(), `"algorithm":"ed25519"`, `"algorithm":"unsupported"`, 1),
		strings.Replace(transitionConfigJSON(), `"publicKeyBase64":"`+base64.StdEncoding.EncodeToString([]byte(strings.Repeat("o", 32)))+`"`, `"publicKeyBase64":"YQ=="`, 1),
	} {
		if _, err := ParsePublisherTrustTransitionConfig([]byte(raw)); err == nil {
			t.Fatalf("invalid transition config was accepted: %s", raw)
		}
	}
}

func TestEpochBoundaryProducesRetryableEvidence(t *testing.T) {
	maxMinusOne := "18446744073709551614"
	max := "18446744073709551615"
	raw := strings.Replace(
		transitionConfigJSON(), `"expectedOwnershipEpoch":"4"`,
		`"expectedOwnershipEpoch":"`+maxMinusOne+`"`, 1,
	)
	config, err := ParsePublisherTrustTransitionConfig([]byte(raw))
	if err != nil {
		t.Fatal(err)
	}
	if config.expectedEpoch != ^uint64(0)-1 {
		t.Fatalf("expected epoch = %d", config.expectedEpoch)
	}
	if _, _, err := MarshalPublisherTrustTransitionOutput(PublisherTrustTransitionOutput{
		Schema: PublisherTrustTransitionOutputSchemaV1, OperationID: config.OperationID,
		PublisherID: config.PublisherID, OwnershipEpoch: max,
		PreviousTrustDigest: "sha256:" + strings.Repeat("a", 64),
		ActiveTrustDigest:   "sha256:" + strings.Repeat("b", 64),
	}); err != nil {
		t.Fatalf("maximum committed epoch evidence: %v", err)
	}
	exhausted := strings.Replace(raw, maxMinusOne, max, 1)
	if _, err := ParsePublisherTrustTransitionConfig([]byte(exhausted)); err == nil {
		t.Fatal("exhausted expected epoch was accepted")
	}
}

func TestApplyRequiresExactPriorTrustAndConfirmsTarget(t *testing.T) {
	config, err := ParsePublisherTrustTransitionConfig([]byte(transitionConfigJSON()))
	if err != nil {
		t.Fatal(err)
	}
	current := registered(config.expectedTrust, 4)
	registerCalls := 0
	output, err := ApplyPublisherTrustTransition(context.Background(), func(context.Context, string) (catalogapp.RegisteredPublisher, error) {
		return current, nil
	}, func(_ context.Context, request catalogapp.PublisherTrustTransitionRequest) (catalogapp.PublisherTrustTransitionResult, error) {
		registerCalls++
		if request.ExpectedOwnershipEpoch != 4 || !trustMatches(request.TargetTrust, config.targetTrust) {
			t.Fatalf("transition request = %#v", request)
		}
		current = registered(request.TargetTrust, 5)
		previousDigest, _ := trustDigest(request.ExpectedTrust)
		activeDigest, _ := trustDigest(request.TargetTrust)
		return catalogapp.PublisherTrustTransitionResult{
			OperationID: request.OperationID, PublisherID: request.TargetTrust.PublisherID,
			ExpectedOwnershipEpoch: 4, OwnershipEpoch: 5,
			PreviousTrustDigest: previousDigest, ActiveTrustDigest: activeDigest,
		}, nil
	}, config)
	if err != nil {
		t.Fatal(err)
	}
	if registerCalls != 1 || output.OwnershipEpoch != "5" || output.PreviousTrustDigest == output.ActiveTrustDigest {
		t.Fatalf("output=%#v registerCalls=%d", output, registerCalls)
	}

	// The same operator operation is idempotent after the exact CAS commits.
	if _, err := ApplyPublisherTrustTransition(context.Background(), func(context.Context, string) (catalogapp.RegisteredPublisher, error) {
		return current, nil
	}, func(_ context.Context, request catalogapp.PublisherTrustTransitionRequest) (catalogapp.PublisherTrustTransitionResult, error) {
		previousDigest, _ := trustDigest(request.ExpectedTrust)
		activeDigest, _ := trustDigest(request.TargetTrust)
		return catalogapp.PublisherTrustTransitionResult{
			OperationID: request.OperationID, PublisherID: request.TargetTrust.PublisherID,
			ExpectedOwnershipEpoch: 4, OwnershipEpoch: 5,
			PreviousTrustDigest: previousDigest, ActiveTrustDigest: activeDigest,
		}, nil
	}, config); err != nil {
		t.Fatalf("exact retry: %v", err)
	}
}

func TestApplyRejectsAuthorityDriftWithoutMutation(t *testing.T) {
	config, err := ParsePublisherTrustTransitionConfig([]byte(transitionConfigJSON()))
	if err != nil {
		t.Fatal(err)
	}
	drifted := registered(config.expectedTrust, 4)
	drifted.Keys[0].PublicKey = []byte("different")
	called := false
	_, err = ApplyPublisherTrustTransition(context.Background(), func(context.Context, string) (catalogapp.RegisteredPublisher, error) {
		return drifted, nil
	}, func(context.Context, catalogapp.PublisherTrustTransitionRequest) (catalogapp.PublisherTrustTransitionResult, error) {
		called = true
		return catalogapp.PublisherTrustTransitionResult{}, catalogapp.ErrConflict
	}, config)
	if !errors.Is(err, catalogapp.ErrConflict) || !called {
		t.Fatalf("drift error=%v registerCalled=%t", err, called)
	}
}

func TestMarshalOutputIsCanonicalAndContentAddressed(t *testing.T) {
	output := PublisherTrustTransitionOutput{Schema: PublisherTrustTransitionOutputSchemaV1, OperationID: "operation-1", PublisherID: "io.nexuscrm", OwnershipEpoch: "5",
		PreviousTrustDigest: "sha256:" + strings.Repeat("a", 64), ActiveTrustDigest: "sha256:" + strings.Repeat("b", 64)}
	first, firstDigest, err := MarshalPublisherTrustTransitionOutput(output)
	if err != nil {
		t.Fatal(err)
	}
	second, secondDigest, err := MarshalPublisherTrustTransitionOutput(output)
	if err != nil {
		t.Fatal(err)
	}
	if string(first) != string(second) || firstDigest != secondDigest {
		t.Fatal("transition output is not deterministic")
	}
}

func TestTrustDigestMatchesDeploymentCanonicalFixture(t *testing.T) {
	publicKey, err := base64.StdEncoding.DecodeString("nNtQW4zuf0BeJdqTikM5tMlkiWZUO6of6U8le0N1Bw0=")
	if err != nil {
		t.Fatal(err)
	}
	validFrom, err := time.Parse(time.RFC3339, "2026-01-01T00:00:00Z")
	if err != nil {
		t.Fatal(err)
	}
	validUntil, err := time.Parse(time.RFC3339, "2036-01-01T00:00:00Z")
	if err != nil {
		t.Fatal(err)
	}
	digest, err := trustDigest(catalogapp.PublisherTrust{
		PublisherID: "io.nexuscrm",
		Active:      true,
		Keys: []catalogapp.PublisherKey{{
			KeyID: "io.nexuscrm.sfcrm/0.35.0/c1056261", Algorithm: "ed25519",
			PublicKey: publicKey, ValidFrom: validFrom, ValidUntil: validUntil,
		}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if digest != "sha256:8cfeba1a97e3bb6c34f0a5296dca783ae39e171148b7844815cc3826a663c86e" {
		t.Fatalf("canonical trust digest = %s", digest)
	}
}
