package catalogapp

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

	"github.com/DATA-DOG/go-sqlmock"
)

func TestAbandonUnpublishedUpgradeRestoresOnlyActiveInstallation(t *testing.T) {
	const (
		operationID    = "018f0000-0000-7000-9000-000000000031"
		installationID = "018f0000-0000-7000-9000-000000000032"
		orgID          = "org-1"
		appID          = "io.example.app"
	)
	evidence := testDigest("abandon-unpublished-upgrade")
	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)
	}

	expectAbandonOperationLocks(
		mock, operationID, installationID, orgID, appID,
		StateUpgrading, "CANDIDATE_COMMITTED", "",
		StateUpgrading, operationID,
	)
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT Step
FROM _System_CatalogAppOperationJournal
WHERE OperationId = ? ORDER BY SequenceNumber FOR UPDATE`)).
		WithArgs(operationID).
		WillReturnRows(sqlmock.NewRows([]string{"Step"}).AddRow("CANDIDATE_COMMITTED"))
	mock.ExpectExec("UPDATE _System_CatalogHandlerBinding").
		WithArgs(operationID).WillReturnResult(sqlmock.NewResult(0, 2))
	mock.ExpectExec("UPDATE _System_CatalogFrontendBinding").
		WithArgs(operationID).WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("DELETE FROM _System_CatalogNamespaceReservation").
		WithArgs(installationID).WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE _System_CatalogNamespaceReservation").
		WithArgs(installationID).WillReturnResult(sqlmock.NewResult(0, 2))
	mock.ExpectExec("UPDATE _System_CatalogAppInstallation").
		WithArgs(installationID, operationID).WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(regexp.QuoteMeta(`UPDATE _System_CatalogOrgState
SET ActiveOperationId = NULL, LastModifiedDate = CURRENT_TIMESTAMP(6)
WHERE OrgId = ? AND ActiveOperationId = ?`)).
		WithArgs(orgID, operationID).WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE _System_CatalogAppOperation").
		WithArgs(OperationStepAbandoned, evidence, operationID).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectQuery("SELECT SequenceNumber FROM _System_CatalogAppOperationJournal").
		WithArgs(operationID).WillReturnRows(
		sqlmock.NewRows([]string{"SequenceNumber"}).AddRow([]byte("1")),
	)
	mock.ExpectExec("INSERT INTO _System_CatalogAppOperationJournal").
		WithArgs(
			operationID, "2", OperationStepAbandoned,
			string(StateFailed), evidence,
		).WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	operation, err := authority.AbandonUnpublishedUpgrade(
		context.Background(), operationID, evidence,
	)
	if err != nil {
		t.Fatal(err)
	}
	if operation.State != StateFailed ||
		operation.Step != OperationStepAbandoned ||
		operation.LastErrorDigest != evidence {
		t.Fatalf("abandoned operation = %#v", operation)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestAbandonUnpublishedUpgradeRejectsMetadataSelection(t *testing.T) {
	const (
		operationID    = "018f0000-0000-7000-9000-000000000041"
		installationID = "018f0000-0000-7000-9000-000000000042"
		orgID          = "org-1"
		appID          = "io.example.app"
	)
	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)
	}
	expectAbandonOperationLocks(
		mock, operationID, installationID, orgID, appID,
		StateUpgrading, "CANDIDATE_COMMITTED", "",
		StateUpgrading, operationID,
	)
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT Step
FROM _System_CatalogAppOperationJournal
WHERE OperationId = ? ORDER BY SequenceNumber FOR UPDATE`)).
		WithArgs(operationID).
		WillReturnRows(sqlmock.NewRows([]string{"Step"}).
			AddRow("CANDIDATE_COMMITTED").
			AddRow(operationJournalStepMetadataSelectionCommitted))
	mock.ExpectRollback()

	_, err = authority.AbandonUnpublishedUpgrade(
		context.Background(), operationID, testDigest("blocked-abandonment"),
	)
	if !errors.Is(err, ErrInvalidTransition) {
		t.Fatalf("metadata-selected abandonment = %v, want invalid transition", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestAbandonUnpublishedUpgradeRejectsPartialJournalRead(t *testing.T) {
	const (
		operationID    = "018f0000-0000-7000-9000-000000000043"
		installationID = "018f0000-0000-7000-9000-000000000044"
		orgID          = "org-1"
		appID          = "io.example.app"
	)
	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)
	}
	expectAbandonOperationLocks(
		mock, operationID, installationID, orgID, appID,
		StateUpgrading, "CANDIDATE_COMMITTED", "",
		StateUpgrading, operationID,
	)
	journalErr := errors.New("journal read interrupted")
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT Step
FROM _System_CatalogAppOperationJournal
WHERE OperationId = ? ORDER BY SequenceNumber FOR UPDATE`)).
		WithArgs(operationID).
		WillReturnRows(sqlmock.NewRows([]string{"Step"}).
			AddRow("CANDIDATE_COMMITTED").
			AddRow("CANDIDATE_COMMITTED").
			RowError(1, journalErr))
	mock.ExpectRollback()

	_, err = authority.AbandonUnpublishedUpgrade(
		context.Background(), operationID, testDigest("blocked-partial-journal"),
	)
	if !errors.Is(err, journalErr) {
		t.Fatalf("partial journal abandonment = %v, want %v", err, journalErr)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func expectAbandonOperationLocks(
	mock sqlmock.Sqlmock,
	operationID string,
	installationID string,
	orgID string,
	appID string,
	operationState InstallationState,
	operationStep string,
	lastError string,
	installationState InstallationState,
	activeOperationID string,
) {
	mock.ExpectBegin()
	expectAppPublicationAdmissionFence(mock)
	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("7"), []byte("11"), operationID, []byte("1")))
	mock.ExpectQuery("SELECT OperationId, Kind, AppInstallationId.*_System_CatalogAppOperation").
		WithArgs(operationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"OperationId", "Kind", "AppInstallationId", "OrgId", "AppId",
			"State", "Step", "TargetReleaseDigest", "ExpectedCatalogRevision",
			"ExpectedSecurityEpoch", "ExpectedProjectionDigest",
			"AcknowledgedSecurityEpoch", "AcknowledgedProjectionDigest", "LastError",
		}).AddRow(
			operationID, OperationUpgrade, installationID, orgID, appID,
			operationState, operationStep, testDigest("target-release"),
			[]byte("8"), []byte("12"), testDigest("projection"), nil, nil, lastError,
		))
	empty := []byte(`[]`)
	mock.ExpectQuery("SELECT AppInstallationId, OrgId, AppId, State.*_System_CatalogAppInstallation").
		WithArgs(installationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"AppInstallationId", "OrgId", "AppId", "State", "ActiveReleaseDigest",
			"CandidateReleaseDigest", "LastKnownGoodReleaseDigest", "CatalogRevision",
			"ActivationEpoch", "ActiveOperationId", "ActiveAuthorizedWorkloads",
			"CandidateAuthorizedWorkloads", "ActiveGrantedCapabilities",
			"CandidateGrantedCapabilities", "ActiveResolvedDependencies",
			"CandidateResolvedDependencies",
		}).AddRow(
			installationID, orgID, appID, installationState,
			testDigest("active-release"), testDigest("target-release"),
			testDigest("last-good"), []byte("7"), []byte("3"), activeOperationID,
			empty, empty, empty, empty, empty, empty,
		))
}
