package catalogapp

import (
	"context"
	"errors"
	"regexp"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	layoutbundlekeyv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundlekeyv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/internal/catalogadmissionfence"
)

type storedNamespaceDeclarationProjection struct {
	appID       string
	release     string
	namespaceID string
}

func TestInspectRegisteredNamespaceDeclarationsReturnsSortedSnapshotAndRollsBack(
	t *testing.T,
) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	release1 := testRegisteredReleaseVersionForApp(
		t,
		"io.example.alpha",
		"1.0.0",
		"Subject",
	)
	release2 := testRegisteredReleaseVersionForApp(
		t,
		"io.example.alpha",
		"2.0.0",
		"Summary",
	)
	expectNamespaceDeclarationInspectionStart(mock)
	expectNamespaceDeclarationIdentities(mock, "io.example.alpha")
	expectNamespaceDeclarationReleases(mock, release1, release2)
	expectNamespaceDeclarationComponents(mock, release1, release2)
	expectNamespaceDeclarationProjections(
		mock,
		storedNamespaceDeclarationProjection{
			appID:       release1.AppID,
			release:     release1.AppReleaseDigest,
			namespaceID: "io.example.issues",
		},
		storedNamespaceDeclarationProjection{
			appID:       release2.AppID,
			release:     release2.AppReleaseDigest,
			namespaceID: "io.example.issues",
		},
	)
	mock.ExpectRollback()

	got, err := authority.InspectRegisteredNamespaceDeclarations(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	want := []ObservedAppNamespaceDeclaration{
		{NamespaceID: "io.example.alpha", AppID: "io.example.alpha"},
		{NamespaceID: "io.example.issues", AppID: "io.example.alpha"},
	}
	if len(got) != len(want) {
		t.Fatalf("snapshot length = %d, want %d: %#v", len(got), len(want), got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("snapshot[%d] = %#v, want %#v", i, got[i], want[i])
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInspectRegisteredNamespaceDeclarationsAcceptsEmptyComponentBundle(
	t *testing.T,
) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	release := testEmptyRegisteredNamespaceRelease(t, "io.example.alpha")
	expectNamespaceDeclarationInspectionStart(mock)
	expectNamespaceDeclarationIdentities(mock, release.AppID)
	expectNamespaceDeclarationReleases(mock, release)
	expectNamespaceDeclarationComponents(mock, release)
	expectNamespaceDeclarationProjections(
		mock,
		storedNamespaceDeclarationProjection{
			appID:       release.AppID,
			release:     release.AppReleaseDigest,
			namespaceID: "io.example.empty",
		},
	)
	mock.ExpectRollback()

	got, err := authority.InspectRegisteredNamespaceDeclarations(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	want := []ObservedAppNamespaceDeclaration{
		{NamespaceID: "io.example.alpha", AppID: "io.example.alpha"},
		{NamespaceID: "io.example.empty", AppID: "io.example.alpha"},
	}
	if len(got) != len(want) {
		t.Fatalf("snapshot length = %d, want %d: %#v", len(got), len(want), got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("snapshot[%d] = %#v, want %#v", i, got[i], want[i])
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInspectRegisteredNamespaceDeclarationsRejectsCrossAppClaim(t *testing.T) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	alpha := testRegisteredReleaseVersionForApp(
		t,
		"io.example.alpha",
		"1.0.0",
		"Subject",
	)
	bravo := testRegisteredReleaseVersionForApp(
		t,
		"io.example.bravo",
		"1.0.0",
		"Subject",
	)
	expectNamespaceDeclarationInspectionStart(mock)
	expectNamespaceDeclarationIdentities(
		mock,
		"io.example.alpha",
		"io.example.bravo",
	)
	expectNamespaceDeclarationReleases(mock, alpha, bravo)
	expectNamespaceDeclarationComponents(mock, alpha, bravo)
	expectNamespaceDeclarationProjections(
		mock,
		storedNamespaceDeclarationProjection{
			appID:       alpha.AppID,
			release:     alpha.AppReleaseDigest,
			namespaceID: "io.example.issues",
		},
		storedNamespaceDeclarationProjection{
			appID:       bravo.AppID,
			release:     bravo.AppReleaseDigest,
			namespaceID: "io.example.issues",
		},
	)
	mock.ExpectRollback()

	if _, err := authority.InspectRegisteredNamespaceDeclarations(context.Background()); !errors.Is(err, ErrConflict) {
		t.Fatalf("cross-App namespace declaration = %v, want ErrConflict", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInspectRegisteredNamespaceDeclarationsRejectsMissingIdentity(t *testing.T) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	release := testRegisteredReleaseVersionForApp(
		t,
		"io.example.alpha",
		"1.0.0",
		"Subject",
	)
	expectNamespaceDeclarationInspectionStart(mock)
	expectNamespaceDeclarationIdentities(mock)
	expectNamespaceDeclarationReleases(mock, release)
	expectNamespaceDeclarationComponents(mock, release)
	expectNamespaceDeclarationProjections(
		mock,
		storedNamespaceDeclarationProjection{
			appID:       release.AppID,
			release:     release.AppReleaseDigest,
			namespaceID: "io.example.issues",
		},
	)
	mock.ExpectRollback()

	if _, err := authority.InspectRegisteredNamespaceDeclarations(context.Background()); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("release without AppIdentity = %v, want ErrCorrupt", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInspectRegisteredNamespaceDeclarationsRejectsProjectionDrift(t *testing.T) {
	for _, test := range []struct {
		name        string
		projections []storedNamespaceDeclarationProjection
	}{
		{
			name: "missing declared namespace",
		},
		{
			name: "extra projected namespace",
			projections: []storedNamespaceDeclarationProjection{{
				namespaceID: "io.example.extra",
			}},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
			defer cleanup()
			release := testRegisteredReleaseVersionForApp(
				t,
				"io.example.alpha",
				"1.0.0",
				"Subject",
			)
			projections := append(
				[]storedNamespaceDeclarationProjection(nil),
				test.projections...,
			)
			for i := range projections {
				projections[i].appID = release.AppID
				projections[i].release = release.AppReleaseDigest
			}
			expectNamespaceDeclarationInspectionStart(mock)
			expectNamespaceDeclarationIdentities(mock, release.AppID)
			expectNamespaceDeclarationReleases(mock, release)
			expectNamespaceDeclarationComponents(mock, release)
			expectNamespaceDeclarationProjections(mock, projections...)
			mock.ExpectRollback()

			if _, err := authority.InspectRegisteredNamespaceDeclarations(context.Background()); !errors.Is(err, ErrCorrupt) {
				t.Fatalf("projection drift = %v, want ErrCorrupt", err)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestInspectRegisteredNamespaceDeclarationsRejectsOrphanProjection(t *testing.T) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	expectNamespaceDeclarationInspectionStart(mock)
	expectNamespaceDeclarationIdentities(mock)
	expectNamespaceDeclarationReleases(mock)
	expectNamespaceDeclarationComponents(mock)
	expectNamespaceDeclarationProjections(
		mock,
		storedNamespaceDeclarationProjection{
			appID:       "io.example.alpha",
			release:     "sha256:" + strings.Repeat("a", 64),
			namespaceID: "io.example.issues",
		},
	)
	mock.ExpectRollback()

	if _, err := authority.InspectRegisteredNamespaceDeclarations(context.Background()); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("orphan AppReleaseNamespace = %v, want ErrCorrupt", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInspectRegisteredNamespaceDeclarationsRejectsOrphanComponent(t *testing.T) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	expectNamespaceDeclarationInspectionStart(mock)
	expectNamespaceDeclarationIdentities(mock)
	expectNamespaceDeclarationReleases(mock)
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT AppId, AppReleaseDigest, ComponentId,
CanonicalBehavior
FROM _System_CatalogAppReleaseComponent
ORDER BY AppId, AppReleaseDigest, ComponentId
FOR UPDATE`)).WillReturnRows(sqlmock.NewRows([]string{
		"AppId",
		"AppReleaseDigest",
		"ComponentId",
		"CanonicalBehavior",
	}).AddRow(
		"io.example.alpha",
		"sha256:"+strings.Repeat("a", 64),
		"sha256:"+strings.Repeat("b", 64),
		[]byte(`{}`),
	))
	expectNamespaceDeclarationProjections(mock)
	mock.ExpectRollback()

	if _, err := authority.InspectRegisteredNamespaceDeclarations(context.Background()); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("orphan AppReleaseComponent = %v, want ErrCorrupt", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInspectRegisteredNamespaceDeclarationsRejectsStoredReleaseTupleDrift(
	t *testing.T,
) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	release := testRegisteredReleaseVersionForApp(
		t,
		"io.example.alpha",
		"1.0.0",
		"Subject",
	)
	release.SemanticVersion = "2.0.0"
	expectNamespaceDeclarationInspectionStart(mock)
	expectNamespaceDeclarationIdentities(mock, release.AppID)
	expectNamespaceDeclarationReleases(mock, release)
	expectNamespaceDeclarationComponents(mock, release)
	expectNamespaceDeclarationProjections(
		mock,
		storedNamespaceDeclarationProjection{
			appID:       release.AppID,
			release:     release.AppReleaseDigest,
			namespaceID: "io.example.issues",
		},
	)
	mock.ExpectRollback()

	if _, err := authority.InspectRegisteredNamespaceDeclarations(context.Background()); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("stored AppRelease tuple drift = %v, want ErrCorrupt", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInspectRegisteredNamespaceDeclarationsRejectsReservedSystemNamespace(
	t *testing.T,
) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	expectNamespaceDeclarationInspectionStart(mock)
	expectNamespaceDeclarationIdentities(
		mock,
		layoutbundlekeyv1.SystemSlotOwnerNamespace,
	)
	mock.ExpectRollback()

	if _, err := authority.InspectRegisteredNamespaceDeclarations(context.Background()); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("reserved system AppIdentity = %v, want ErrCorrupt", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInspectRegisteredNamespaceDeclarationsJoinsRollbackFailure(t *testing.T) {
	authority, mock, cleanup := newNamespaceDeclarationInspectionAuthority(t)
	defer cleanup()
	inspectionErr := errors.New("source fence failure")
	rollbackErr := errors.New("rollback failure")
	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT FenceKey, FenceGeneration
FROM _System_CatalogPublicationAdmissionFence
WHERE FenceKey = ? FOR UPDATE`)).
		WithArgs(catalogadmissionfence.Key).
		WillReturnError(inspectionErr)
	mock.ExpectRollback().WillReturnError(rollbackErr)

	entries, err := authority.InspectRegisteredNamespaceDeclarations(context.Background())
	if entries != nil {
		t.Fatalf("entries = %#v, want nil", entries)
	}
	if !errors.Is(err, inspectionErr) || !errors.Is(err, rollbackErr) {
		t.Fatalf("inspection error = %v, want both failures", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func newNamespaceDeclarationInspectionAuthority(
	t *testing.T,
) (*Authority, sqlmock.Sqlmock, func()) {
	t.Helper()
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	authority, err := New(db, Options{})
	if err != nil {
		_ = db.Close()
		t.Fatal(err)
	}
	return authority, mock, func() { _ = db.Close() }
}

func expectNamespaceDeclarationInspectionStart(mock sqlmock.Sqlmock) {
	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT FenceKey, FenceGeneration
FROM _System_CatalogPublicationAdmissionFence
WHERE FenceKey = ? FOR UPDATE`)).
		WithArgs(catalogadmissionfence.Key).
		WillReturnRows(sqlmock.NewRows([]string{
			"FenceKey",
			"FenceGeneration",
		}).AddRow(catalogadmissionfence.Key, []byte("0")))
}

func expectNamespaceDeclarationIdentities(mock sqlmock.Sqlmock, appIDs ...string) {
	rows := sqlmock.NewRows([]string{"AppId"})
	for _, appID := range appIDs {
		rows.AddRow(appID)
	}
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT AppId
FROM _System_CatalogAppIdentity
ORDER BY AppId
FOR UPDATE`)).WillReturnRows(rows)
}

func expectNamespaceDeclarationReleases(
	mock sqlmock.Sqlmock,
	releases ...RegisteredRelease,
) {
	rows := sqlmock.NewRows([]string{
		"AppId",
		"AppReleaseDigest",
		"PublisherId",
		"SemanticVersion",
		"MetadataBundleDigest",
		"CanonicalManifest",
		"CanonicalBundle",
	})
	for _, release := range releases {
		rows.AddRow(
			release.AppID,
			release.AppReleaseDigest,
			release.PublisherID,
			release.SemanticVersion,
			release.MetadataBundleDigest,
			release.Package.CanonicalManifest,
			release.Package.CanonicalBundle,
		)
	}
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT AppId, AppReleaseDigest, PublisherId,
SemanticVersion, MetadataBundleDigest, CanonicalManifest, CanonicalBundle
FROM _System_CatalogAppRelease
ORDER BY AppId, AppReleaseDigest
FOR UPDATE`)).WillReturnRows(rows)
}

func expectNamespaceDeclarationComponents(
	mock sqlmock.Sqlmock,
	releases ...RegisteredRelease,
) {
	rows := sqlmock.NewRows([]string{
		"AppId",
		"AppReleaseDigest",
		"ComponentId",
		"CanonicalBehavior",
	})
	for _, release := range releases {
		for _, component := range release.Package.ComponentBlobs {
			rows.AddRow(
				release.AppID,
				release.AppReleaseDigest,
				component.ComponentID,
				component.CanonicalBehavior,
			)
		}
	}
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT AppId, AppReleaseDigest, ComponentId,
CanonicalBehavior
FROM _System_CatalogAppReleaseComponent
ORDER BY AppId, AppReleaseDigest, ComponentId
FOR UPDATE`)).WillReturnRows(rows)
}

func expectNamespaceDeclarationProjections(
	mock sqlmock.Sqlmock,
	projections ...storedNamespaceDeclarationProjection,
) {
	rows := sqlmock.NewRows([]string{
		"AppId",
		"AppReleaseDigest",
		"NamespaceId",
	})
	for _, projection := range projections {
		rows.AddRow(projection.appID, projection.release, projection.namespaceID)
	}
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT AppId, AppReleaseDigest, NamespaceId
FROM _System_CatalogAppReleaseNamespace
ORDER BY AppId, AppReleaseDigest, NamespaceId
FOR UPDATE`)).WillReturnRows(rows)
}

func testEmptyRegisteredNamespaceRelease(
	t *testing.T,
	appID string,
) RegisteredRelease {
	t.Helper()
	manifest := appauthority.AppManifest{
		SchemaVersion:              appauthority.AppManifestSchemaVersion,
		AppID:                      appID,
		PublisherID:                "io.example",
		SemanticVersion:            "1.0.0",
		MetadataBundleDigest:       testDigest("placeholder"),
		Dependencies:               []appauthority.Dependency{},
		OwnedNamespaces:            []string{"io.example.empty"},
		RequiredProtocols:          []appauthority.ProtocolRequirement{},
		RequiredEngineCapabilities: []string{"runtime.records.read.v1"},
		ProvidedCapabilities:       []appauthority.ProvidedCapability{},
		FrontendCompatibility: appauthority.FrontendCompatibility{
			Mode: appauthority.FrontendModeNone,
		},
	}
	bundle := appauthority.MetadataBundle{
		SchemaVersion: appauthority.MetadataBundleSchemaVersion,
		Components:    []appauthority.Component{},
	}
	metadataDigest, err := appauthority.MetadataBundleDigest(bundle, manifest)
	if err != nil {
		t.Fatal(err)
	}
	manifest.MetadataBundleDigest = metadataDigest
	verified, err := appauthority.VerifyPackage(appauthority.Package{
		Manifest:       manifest,
		Bundle:         bundle,
		ComponentBlobs: []appauthority.ComponentBlob{},
	})
	if err != nil {
		t.Fatal(err)
	}
	return registeredRelease(verified)
}
