package catalogapp

import (
	"context"
	"database/sql"
	"errors"
	"regexp"
	"testing"

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

func TestReadOperationByIdempotencyReturnsNotFoundWithoutFallback(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)
	}
	query := regexp.QuoteMeta(`SELECT OperationId FROM _System_CatalogAppOperation
WHERE OrgId = ? AND AppId = ? AND IdempotencyKey = ?`)
	mock.ExpectQuery(query).WithArgs("org-1", "io.example.app", "retry-1").WillReturnError(sql.ErrNoRows)
	if _, err := authority.ReadOperationByIdempotency(context.Background(), "org-1", "io.example.app", "retry-1"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("read missing idempotency operation = %v, want ErrNotFound", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadActiveHandlerBindingUsesCatalogActiveOwner(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)
	}

	query := regexp.QuoteMeta(`SELECT HandlerBindingId, OrgId, AppInstallationId, AppReleaseDigest,
CapabilityId, HandlerRef, WorkloadId, ServiceReleaseDigest, OwnershipEpoch, State, CompatibilityReceiptDigest
FROM _System_CatalogHandlerBinding
WHERE AppInstallationId = ? AND CapabilityId = ? AND State = 'ACTIVE' AND ActiveKey IS NOT NULL`)
	columns := []string{"HandlerBindingId", "OrgId", "AppInstallationId", "AppReleaseDigest", "CapabilityId",
		"HandlerRef", "WorkloadId", "ServiceReleaseDigest", "OwnershipEpoch", "State", "CompatibilityReceiptDigest"}
	mock.ExpectQuery(query).WithArgs("installation-1", "runtime.records.evaluate-access.v1").
		WillReturnRows(sqlmock.NewRows(columns).AddRow("binding-1", "org-1", "installation-1", "sha256:app",
			"runtime.records.evaluate-access.v1", "handler-1", "workload-1", "sha256:service", []byte("7"),
			BindingActive, "sha256:receipt"))

	binding, err := authority.ReadActiveHandlerBinding(context.Background(), "installation-1", "runtime.records.evaluate-access.v1")
	if err != nil {
		t.Fatal(err)
	}
	if binding.HandlerBindingID != "binding-1" || binding.WorkloadID != "workload-1" || binding.OwnershipEpoch != 7 || binding.State != BindingActive {
		t.Fatalf("unexpected active binding: %#v", binding)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadActiveHandlerBindingDoesNotFallBackToInactiveOwner(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)
	}

	mock.ExpectQuery("SELECT HandlerBindingId, OrgId, AppInstallationId").
		WithArgs("installation-1", "runtime.records.evaluate-access.v1").
		WillReturnError(sql.ErrNoRows)
	_, err = authority.ReadActiveHandlerBinding(context.Background(), "installation-1", "runtime.records.evaluate-access.v1")
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("ReadActiveHandlerBinding error = %v, want ErrNotFound", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadLatestHandlerOwnershipEpochIncludesFailedCandidates(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)
	}
	mock.ExpectQuery(`(?s)SELECT OwnershipEpoch FROM _System_CatalogHandlerBinding.*ORDER BY OwnershipEpoch DESC LIMIT 1`).
		WithArgs("installation-1", "app.record-access.v1").
		WillReturnRows(sqlmock.NewRows([]string{"OwnershipEpoch"}).AddRow("7"))
	epoch, err := authority.ReadLatestHandlerOwnershipEpoch(context.Background(), "installation-1", "app.record-access.v1")
	if err != nil || epoch != 7 {
		t.Fatalf("latest ownership epoch = %d, %v; want 7", epoch, err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadOperationHandlerBindingRecoversHistoricalSelection(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)
	}
	columns := []string{"HandlerBindingId", "OrgId", "AppInstallationId", "AppReleaseDigest", "CapabilityId",
		"HandlerRef", "WorkloadId", "ServiceReleaseDigest", "OwnershipEpoch", "State", "CompatibilityReceiptDigest"}
	mock.ExpectQuery(`(?s)FROM _System_CatalogHandlerBinding WHERE OperationId = \? AND CapabilityId = \?.*ORDER BY HandlerBindingId`).
		WithArgs("operation-1", "app.record-access.v1").
		WillReturnRows(sqlmock.NewRows(columns).AddRow("binding-1", "org-1", "installation-1", testDigest("app"),
			"app.record-access.v1", "handler-1", "workload-1", testDigest("service"), "3", BindingDraining, testDigest("receipt")))
	binding, err := authority.ReadOperationHandlerBinding(context.Background(), "operation-1", "app.record-access.v1")
	if err != nil {
		t.Fatal(err)
	}
	if binding.HandlerBindingID != "binding-1" || binding.OwnershipEpoch != 3 || binding.State != BindingDraining {
		t.Fatalf("operation binding = %#v", binding)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestCommittedOperationBindingCannotBeFailedOutOfBand(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)
	}
	mock.ExpectExec(`(?s)UPDATE _System_CatalogHandlerBinding b.*LEFT JOIN _System_CatalogAppOperation o.*SET b.State = 'FAILED'.*b.OperationId IS NULL OR o.Step = 'CANDIDATE_COMMITTED'`).
		WithArgs("binding-1").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectQuery(`(?s)FROM _System_CatalogHandlerBinding WHERE HandlerBindingId = \?`).
		WithArgs("binding-1").WillReturnRows(sqlmock.NewRows([]string{
		"HandlerBindingId", "OrgId", "AppInstallationId", "AppReleaseDigest", "CapabilityId", "HandlerRef",
		"WorkloadId", "ServiceReleaseDigest", "OwnershipEpoch", "State", "CompatibilityReceiptDigest",
	}).AddRow("binding-1", "org-1", "installation-1", testDigest("app"), "app.record-access.v1", "handler-1",
		"workload-1", testDigest("service"), "1", BindingStaged, testDigest("receipt")))
	if err := authority.FailStagedHandlerBinding(context.Background(), "binding-1"); !errors.Is(err, ErrInvalidTransition) {
		t.Fatalf("fail committed operation binding = %v, want invalid transition", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadActiveFrontendBindingUsesCatalogActiveOwnerOnly(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)
	}
	query := regexp.QuoteMeta(`SELECT FrontendBindingId, OrgId, AppInstallationId, AppReleaseDigest,
FrontendReleaseDigest, State, CompatibilityReceiptDigest FROM _System_CatalogFrontendBinding
WHERE AppInstallationId = ? AND State = 'ACTIVE' AND ActiveKey IS NOT NULL`)
	columns := []string{"FrontendBindingId", "OrgId", "AppInstallationId", "AppReleaseDigest",
		"FrontendReleaseDigest", "State", "CompatibilityReceiptDigest"}
	mock.ExpectQuery(query).WithArgs("installation-1").WillReturnRows(sqlmock.NewRows(columns).AddRow(
		"frontend-1", "org-1", "installation-1", "sha256:app", "sha256:frontend", BindingActive, "sha256:receipt"))
	binding, err := authority.ReadActiveFrontendBinding(context.Background(), "installation-1")
	if err != nil {
		t.Fatal(err)
	}
	if binding.FrontendBindingID != "frontend-1" || binding.State != BindingActive {
		t.Fatalf("unexpected active frontend binding: %#v", binding)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}
