package catalogapp

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

	"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"
)

func TestReserveAppIDRejectsReservedSystemNamespaceBeforeMutation(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db, Options{})
	if err != nil {
		t.Fatal(err)
	}

	err = authority.ReserveAppID(
		context.Background(),
		"io.example.publisher",
		layoutbundlekeyv1.SystemSlotOwnerNamespace,
		1,
	)
	if !errors.Is(err, ErrConflict) {
		t.Fatalf("reserved system AppID = %v, want ErrConflict", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestAppOperationRejectsReservedSystemOwnedNamespaceBeforeMutation(
	t *testing.T,
) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	mock.ExpectBegin()
	tx, err := db.BeginTx(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}
	if err := reserveNamespaces(
		context.Background(),
		tx,
		"org-1",
		"018f0000-0000-7000-9000-000000000011",
		"io.example.app",
		"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
		[]string{layoutbundlekeyv1.SystemSlotOwnerNamespace},
	); !errors.Is(err, ErrConflict) {
		t.Fatalf("reserved system owned namespace = %v, want ErrConflict", err)
	}
	mock.ExpectRollback()
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestVerifiedReleaseRegistrationRejectsReservedSystemOwnedNamespaceBeforeMutation(
	t *testing.T,
) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db, Options{})
	if err != nil {
		t.Fatal(err)
	}

	base := testRegisteredRelease(t)
	pkg := base.Package.Package
	pkg.Manifest.OwnedNamespaces = []string{
		layoutbundlekeyv1.SystemSlotOwnerNamespace,
	}
	pkg.Bundle.Components[0].NamespaceID =
		layoutbundlekeyv1.SystemSlotOwnerNamespace
	pkg.Bundle.Components[0].ComponentID = appauthority.ComponentID(
		pkg.Bundle.Components[0].NamespaceID,
		pkg.Bundle.Components[0].ComponentKind,
		pkg.Bundle.Components[0].BusinessKey,
	)
	pkg.ComponentBlobs[0].ComponentID = pkg.Bundle.Components[0].ComponentID
	metadataDigest, err := appauthority.MetadataBundleDigest(
		pkg.Bundle,
		pkg.Manifest,
	)
	if err != nil {
		t.Fatal(err)
	}
	pkg.Manifest.MetadataBundleDigest = metadataDigest
	verified, err := appauthority.VerifyPackage(pkg)
	if err != nil {
		t.Fatal(err)
	}
	release := registeredRelease(verified)
	registration := testVerifiedReleaseRegistration(t, release, time.Now().UTC())

	if _, err := authority.registerVerifiedRelease(
		context.Background(),
		registration,
	); err == nil {
		t.Fatal("reserved system owned namespace was registered")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReserveAppIDLocksAdmissionFenceBeforePublisher(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db, Options{})
	if err != nil {
		t.Fatal(err)
	}
	const publisherID = "io.example.publisher"
	const appID = "io.example.app"

	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")))
	mock.ExpectQuery(regexp.QuoteMeta(
		`SELECT OwnershipEpoch, State FROM _System_CatalogPublisher WHERE PublisherId = ? FOR UPDATE`,
	)).WithArgs(publisherID).WillReturnRows(sqlmock.NewRows([]string{
		"OwnershipEpoch",
		"State",
	}).AddRow([]byte("1"), "ACTIVE"))
	mock.ExpectExec("INSERT INTO _System_CatalogAppIdentity").
		WithArgs(appID, publisherID).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	if err := authority.ReserveAppID(
		context.Background(),
		publisherID,
		appID,
		1,
	); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestVerifiedReleaseRegistrationLocksAdmissionFenceBeforeAppIdentity(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db, Options{})
	if err != nil {
		t.Fatal(err)
	}
	release := testRegisteredRelease(t)
	registration := testVerifiedReleaseRegistration(t, release, time.Now().UTC())
	storageErr := errors.New("stop after release admission lock-order assertion")

	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")))
	mock.ExpectQuery(regexp.QuoteMeta(
		`SELECT PublisherId FROM _System_CatalogAppIdentity WHERE AppId = ? FOR UPDATE`,
	)).WithArgs(release.AppID).WillReturnError(storageErr)
	mock.ExpectRollback()

	if _, err := authority.registerVerifiedRelease(
		context.Background(),
		registration,
	); !errors.Is(err, storageErr) {
		t.Fatalf("verified release registration after admission fence = %v, want storage error", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestCompleteOperationLocksWriterBeforeOperationState(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db, Options{})
	if err != nil {
		t.Fatal(err)
	}
	const operationID = "018f0000-0000-7000-9000-000000000021"
	const orgID = "org-1"
	storageErr := errors.New("stop after lock-order assertion")

	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")))
	mock.ExpectQuery(regexp.QuoteMeta(
		`SELECT OrgId FROM _System_CatalogAppOperation WHERE OperationId = ?`,
	)).
		WithArgs(operationID).
		WillReturnRows(sqlmock.NewRows([]string{"OrgId"}).AddRow(orgID))
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT OrgId, RuntimeAudience, CatalogRevision, LastSecurityEpoch, ActiveOperationId, WriterGeneration
FROM _System_CatalogOrgState WHERE OrgId = ? FOR UPDATE`)).
		WithArgs(orgID).
		WillReturnRows(sqlmock.NewRows([]string{
			"OrgId",
			"RuntimeAudience",
			"CatalogRevision",
			"LastSecurityEpoch",
			"ActiveOperationId",
			"WriterGeneration",
		}).AddRow(orgID, "runtime.example", []byte("1"), []byte("1"), operationID, []byte("1")))
	mock.ExpectQuery("SELECT OperationId.*_System_CatalogAppOperation").
		WithArgs(operationID).
		WillReturnError(storageErr)
	mock.ExpectRollback()

	if _, err := authority.CompleteOperation(
		context.Background(),
		operationID,
		nil,
	); !errors.Is(err, storageErr) {
		t.Fatalf("CompleteOperation after writer lock = %v, want storage error", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestResumeOperationLocksWriterBeforeOperationState(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db, Options{})
	if err != nil {
		t.Fatal(err)
	}
	const operationID = "018f0000-0000-7000-9000-000000000022"
	const orgID = "org-1"
	storageErr := errors.New("stop after lock-order assertion")

	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta(
		`SELECT OrgId FROM _System_CatalogAppOperation WHERE OperationId = ?`,
	)).
		WithArgs(operationID).
		WillReturnRows(sqlmock.NewRows([]string{"OrgId"}).AddRow(orgID))
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT OrgId, RuntimeAudience, CatalogRevision, LastSecurityEpoch, ActiveOperationId, WriterGeneration
FROM _System_CatalogOrgState WHERE OrgId = ? FOR UPDATE`)).
		WithArgs(orgID).
		WillReturnRows(sqlmock.NewRows([]string{
			"OrgId",
			"RuntimeAudience",
			"CatalogRevision",
			"LastSecurityEpoch",
			"ActiveOperationId",
			"WriterGeneration",
		}).AddRow(orgID, "runtime.example", []byte("1"), []byte("1"), operationID, []byte("1")))
	mock.ExpectQuery("SELECT OperationId.*_System_CatalogAppOperation").
		WithArgs(operationID).
		WillReturnError(storageErr)
	mock.ExpectRollback()

	if _, err := authority.ResumeOperation(
		context.Background(),
		operationID,
	); !errors.Is(err, storageErr) {
		t.Fatalf("ResumeOperation after writer lock = %v, want storage error", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}
