package catalogreconcile

import (
	"context"
	"errors"
	"testing"
	"time"

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

type fakeReleaseAuthority struct {
	publisher      catalogapp.RegisteredPublisher
	readErr        error
	release        catalogapp.RegisteredRelease
	releaseReadErr error
	verifyErr      error
	verifications  int
	reads          int
	releaseReads   int
	registers      int
	reservations   int
	imports        int
}

type readOnlyReleaseCatalog struct {
	release catalogapp.RegisteredRelease
	err     error
	reads   int
}

func (c *readOnlyReleaseCatalog) ReadRelease(context.Context, string, string) (catalogapp.RegisteredRelease, error) {
	c.reads++
	return c.release, c.err
}

func (a *fakeReleaseAuthority) ReadRelease(context.Context, string, string) (catalogapp.RegisteredRelease, error) {
	a.releaseReads++
	return a.release, a.releaseReadErr
}

func (a *fakeReleaseAuthority) ReadPublisher(context.Context, string) (catalogapp.RegisteredPublisher, error) {
	a.reads++
	return a.publisher, a.readErr
}

func (a *fakeReleaseAuthority) VerifyReleaseCandidate(context.Context, []byte, []byte, catalogapp.PublisherTrust, string, releaseartifact.HostCompatibility) (catalogapp.VerifiedReleaseCandidate, error) {
	a.verifications++
	if a.verifyErr != nil {
		return catalogapp.VerifiedReleaseCandidate{}, a.verifyErr
	}
	return fixtureVerifiedReleaseCandidate(), nil
}

func (a *fakeReleaseAuthority) RegisterPublisher(_ context.Context, trust catalogapp.PublisherTrust, expected uint64) (uint64, error) {
	a.registers++
	return expected + 1, nil
}

func (a *fakeReleaseAuthority) ReserveAppID(context.Context, string, string, uint64) error {
	a.reservations++
	return nil
}

func (a *fakeReleaseAuthority) ImportReleaseArtifact(context.Context, []byte, []byte, releaseartifact.HostCompatibility) (catalogapp.RegisteredRelease, error) {
	a.imports++
	return fixtureProvisionedRelease(), nil
}

func TestResolveReleaseUsesRegisteredCatalogFactWithoutImportMaterial(t *testing.T) {
	// This reader intentionally has no publisher registration, AppID
	// reservation, or release import methods. The registered-release lifecycle
	// path must not require onboarding authority.
	reader := &readOnlyReleaseCatalog{release: fixtureRegisteredRelease()}
	loaded := 0
	release, err := ResolveRelease(context.Background(), reader.ReadRelease, "io.nexuscrm.sfcrm", digestOf('a'), fixtureHostCompatibility(), func(context.Context) (catalogapp.RegisteredRelease, error) {
		loaded++
		return catalogapp.RegisteredRelease{}, errors.New("external release material must not be loaded")
	})
	if err != nil {
		t.Fatalf("resolve registered release: %v", err)
	}
	if release.AppReleaseDigest != digestOf('a') || reader.reads != 1 || loaded != 0 {
		t.Fatalf("release=%#v calls releaseRead=%d provision=%d", release, reader.reads, loaded)
	}
}

func TestResolveReleaseImportsOnlyWhenCatalogReleaseIsMissing(t *testing.T) {
	authority := &fakeReleaseAuthority{
		releaseReadErr: catalogapp.ErrNotFound,
		readErr:        catalogapp.ErrNotFound,
	}
	loaded := 0
	release, err := ResolveRelease(context.Background(), authority.ReadRelease, "io.nexuscrm.sfcrm", digestOf('a'), fixtureHostCompatibility(), func(ctx context.Context) (catalogapp.RegisteredRelease, error) {
		loaded++
		return ProvisionRelease(ctx, authority, ReleaseInput{
			Publisher: fixturePublisherTrust(), AppID: "io.nexuscrm.sfcrm", ExpectedAppReleaseDigest: digestOf('a'),
			PackageBytes: []byte("package"), Attestation: []byte("attestation"),
			Compatibility: fixtureHostCompatibility(),
		})
	})
	if err != nil {
		t.Fatalf("resolve missing release: %v", err)
	}
	if release.AppReleaseDigest != digestOf('a') || authority.releaseReads != 1 || loaded != 1 ||
		authority.verifications != 1 || authority.registers != 1 || authority.reservations != 1 || authority.imports != 1 {
		t.Fatalf("release=%#v calls releaseRead=%d load=%d verify=%d register=%d reserve=%d import=%d",
			release, authority.releaseReads, loaded, authority.verifications, authority.registers, authority.reservations, authority.imports)
	}
}

func TestResolveReleaseReadFailureDoesNotFallBackToExternalMaterial(t *testing.T) {
	authority := &fakeReleaseAuthority{releaseReadErr: errors.New("catalog unavailable")}
	loaded := 0
	if _, err := ResolveRelease(context.Background(), authority.ReadRelease, "io.nexuscrm.sfcrm", digestOf('a'), fixtureHostCompatibility(), func(context.Context) (catalogapp.RegisteredRelease, error) {
		loaded++
		return catalogapp.RegisteredRelease{}, nil
	}); err == nil || !errors.Is(err, authority.releaseReadErr) {
		t.Fatalf("read failure = %v", err)
	}
	if loaded != 0 || authority.verifications != 0 || authority.imports != 0 {
		t.Fatalf("calls load=%d verify=%d import=%d", loaded, authority.verifications, authority.imports)
	}
}

func TestResolveReleaseRechecksCurrentHostCompatibility(t *testing.T) {
	authority := &fakeReleaseAuthority{release: fixtureRegisteredRelease()}
	incompatible := fixtureHostCompatibility()
	incompatible.EngineCapabilities = []string{}
	if _, err := ResolveRelease(context.Background(), authority.ReadRelease, "io.nexuscrm.sfcrm", digestOf('a'), incompatible, nil); err == nil {
		t.Fatal("registered release incompatible with current host was accepted")
	}
	if authority.releaseReads != 1 || authority.imports != 0 {
		t.Fatalf("calls releaseRead=%d import=%d", authority.releaseReads, authority.imports)
	}
}

func TestResolveReleaseMissingWithoutImportMaterialFailsClosed(t *testing.T) {
	authority := &fakeReleaseAuthority{releaseReadErr: catalogapp.ErrNotFound}
	if _, err := ResolveRelease(context.Background(), authority.ReadRelease, "io.nexuscrm.sfcrm", digestOf('a'), fixtureHostCompatibility(), nil); err == nil {
		t.Fatal("missing release without import material was accepted")
	}
	if authority.verifications != 0 || authority.imports != 0 {
		t.Fatalf("calls verify=%d import=%d", authority.verifications, authority.imports)
	}
}

func TestResolveReleaseRejectsProvisionedReleaseOutsidePinnedEnvelope(t *testing.T) {
	reader := &readOnlyReleaseCatalog{err: catalogapp.ErrNotFound}
	cases := []struct {
		name   string
		mutate func(*catalogapp.RegisteredRelease)
	}{
		{name: "identity", mutate: func(release *catalogapp.RegisteredRelease) { release.AppReleaseDigest = digestOf('9') }},
		{name: "compatibility", mutate: func(release *catalogapp.RegisteredRelease) {
			release.Package.Manifest.RequiredEngineCapabilities = []string{"runtime.records.unavailable.v1"}
		}},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			if _, err := ResolveRelease(context.Background(), reader.ReadRelease, "io.nexuscrm.sfcrm", digestOf('a'), fixtureHostCompatibility(), func(context.Context) (catalogapp.RegisteredRelease, error) {
				release := fixtureRegisteredRelease()
				testCase.mutate(&release)
				return release, nil
			}); err == nil {
				t.Fatal("provisioned release outside the pinned envelope was accepted")
			}
		})
	}
}

func fixtureHostCompatibility() releaseartifact.HostCompatibility {
	return releaseartifact.HostCompatibility{
		Protocols:          []releaseartifact.SupportedProtocol{},
		EngineCapabilities: []string{"runtime.records.evaluate-access.v1"},
		ComponentKinds:     []releaseartifact.SupportedComponentKind{},
	}
}

func TestProvisionReleaseReusesExactPublisherTrustAndRejectsImplicitRotation(t *testing.T) {
	trust := fixturePublisherTrust()
	authority := &fakeReleaseAuthority{publisher: catalogapp.RegisteredPublisher{
		PublisherID: trust.PublisherID, OwnershipEpoch: 7, Active: true,
		Keys: []catalogapp.RegisteredPublisherKey{{
			PublisherKey: trust.Keys[0], Active: true,
		}},
	}}
	release, err := ProvisionRelease(context.Background(), authority, ReleaseInput{
		Publisher: trust, AppID: "io.nexuscrm.sfcrm", ExpectedAppReleaseDigest: digestOf('a'), PackageBytes: []byte("package"),
		Attestation: []byte("attestation"), Compatibility: releaseartifact.HostCompatibility{
			Protocols: []releaseartifact.SupportedProtocol{}, EngineCapabilities: []string{},
			ComponentKinds: []releaseartifact.SupportedComponentKind{},
		},
	})
	if err != nil {
		t.Fatalf("provision release: %v", err)
	}
	if release.AppID != "io.nexuscrm.sfcrm" || authority.verifications != 1 || authority.registers != 0 || authority.reservations != 1 || authority.imports != 1 {
		t.Fatalf("release = %#v; calls verify=%d register=%d reserve=%d import=%d", release, authority.verifications, authority.registers, authority.reservations, authority.imports)
	}

	authority.publisher.Keys[0].PublicKey = []byte("different")
	if _, err := ProvisionRelease(context.Background(), authority, ReleaseInput{
		Publisher: trust, AppID: "io.nexuscrm.sfcrm", ExpectedAppReleaseDigest: digestOf('a'),
	}); !errors.Is(err, catalogapp.ErrConflict) {
		t.Fatalf("implicit publisher rotation error = %v, want conflict", err)
	}
	if authority.registers != 0 || authority.reservations != 1 || authority.imports != 1 {
		t.Fatalf("calls register=%d reserve=%d import=%d", authority.registers, authority.reservations, authority.imports)
	}
}

func TestProvisionReleaseVerificationFailureHasNoCatalogSideEffects(t *testing.T) {
	authority := &fakeReleaseAuthority{verifyErr: errors.New("invalid publisher signature")}
	if _, err := ProvisionRelease(context.Background(), authority, ReleaseInput{
		Publisher: fixturePublisherTrust(), AppID: "io.nexuscrm.sfcrm", ExpectedAppReleaseDigest: digestOf('a'),
		PackageBytes: []byte("package"), Attestation: []byte("attestation"),
	}); err == nil {
		t.Fatal("invalid release candidate was accepted")
	}
	if authority.verifications != 1 || authority.reads != 0 || authority.registers != 0 || authority.reservations != 0 || authority.imports != 0 {
		t.Fatalf("calls verify=%d read=%d register=%d reserve=%d import=%d", authority.verifications, authority.reads, authority.registers, authority.reservations, authority.imports)
	}
}

func TestProvisionReleaseDigestMismatchHasNoCatalogSideEffects(t *testing.T) {
	authority := &fakeReleaseAuthority{}
	if _, err := ProvisionRelease(context.Background(), authority, ReleaseInput{
		Publisher: fixturePublisherTrust(), AppID: "io.nexuscrm.sfcrm", ExpectedAppReleaseDigest: digestOf('9'),
		PackageBytes: []byte("package"), Attestation: []byte("attestation"),
	}); err == nil {
		t.Fatal("release candidate outside the pinned digest was accepted")
	}
	if authority.verifications != 1 || authority.reads != 0 || authority.registers != 0 || authority.reservations != 0 || authority.imports != 0 {
		t.Fatalf("calls verify=%d read=%d register=%d reserve=%d import=%d", authority.verifications, authority.reads, authority.registers, authority.reservations, authority.imports)
	}
}

func TestProvisionReleaseRegistersMissingPublisherOnce(t *testing.T) {
	authority := &fakeReleaseAuthority{readErr: catalogapp.ErrNotFound}
	if _, err := ProvisionRelease(context.Background(), authority, ReleaseInput{
		Publisher: fixturePublisherTrust(), AppID: "io.nexuscrm.sfcrm", ExpectedAppReleaseDigest: digestOf('a'),
	}); err != nil {
		t.Fatalf("provision new publisher: %v", err)
	}
	if authority.registers != 1 || authority.reservations != 1 || authority.imports != 1 {
		t.Fatalf("calls register=%d reserve=%d import=%d", authority.registers, authority.reservations, authority.imports)
	}
}

func TestProvisionReleaseIgnoresHistoricalRevokedPublisherKeys(t *testing.T) {
	trust := fixturePublisherTrust()
	authority := &fakeReleaseAuthority{publisher: catalogapp.RegisteredPublisher{
		PublisherID: trust.PublisherID, OwnershipEpoch: 9, Active: true,
		Keys: []catalogapp.RegisteredPublisherKey{
			{PublisherKey: catalogapp.PublisherKey{
				KeyID: "release-retired", Algorithm: "Ed25519", PublicKey: []byte("retired-key"),
				ValidFrom:  time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
				ValidUntil: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
			}, Active: false},
			{PublisherKey: trust.Keys[0], Active: true},
		},
	}}
	if _, err := ProvisionRelease(context.Background(), authority, ReleaseInput{
		Publisher: trust, AppID: "io.nexuscrm.sfcrm", ExpectedAppReleaseDigest: digestOf('a'),
	}); err != nil {
		t.Fatalf("provision after publisher key rotation: %v", err)
	}
	if authority.registers != 0 || authority.reservations != 1 || authority.imports != 1 {
		t.Fatalf("calls register=%d reserve=%d import=%d", authority.registers, authority.reservations, authority.imports)
	}
}

func fixturePublisherTrust() catalogapp.PublisherTrust {
	return catalogapp.PublisherTrust{
		PublisherID: "io.nexuscrm", Active: true,
		Keys: []catalogapp.PublisherKey{{
			KeyID: "release-1", Algorithm: "Ed25519", PublicKey: []byte("public-key"),
			ValidFrom:  time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
			ValidUntil: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC),
		}},
	}
}

func fixtureProvisionedRelease() catalogapp.RegisteredRelease {
	release := fixtureRegisteredRelease()
	release.PublisherID = "io.nexuscrm"
	release.SemanticVersion = "0.2.0"
	release.MetadataBundleDigest = digestOf('b')
	return release
}

func fixtureVerifiedReleaseCandidate() catalogapp.VerifiedReleaseCandidate {
	return catalogapp.VerifiedReleaseCandidate{
		AppID: "io.nexuscrm.sfcrm", PublisherID: "io.nexuscrm", SemanticVersion: "0.2.0",
		AppReleaseDigest: digestOf('a'), MetadataBundleDigest: digestOf('b'),
	}
}
