package catalogapp

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"testing"

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

func TestActiveHandlerProjectionFencesServiceOnlyBindingRotations(t *testing.T) {
	const (
		operationID = "00000000-0000-0000-0000-000000000099"
		selectedID  = "app.catalog-serving.v1"
		workloadID  = "app.workload-only.v1"
	)
	installation := installationRecord{
		ID: "00000000-0000-0000-0000-000000000001", OrgID: "org-test",
		AppID: "io.example.app", ActiveRelease: testDigest("release"),
		CatalogRevision: 7, ActiveWorkloads: []string{"workload-1"},
		ActiveCapabilities: []string{"runtime.metadata.read-admin.v1", "runtime.records.read.v1"},
	}
	selectedJSON, err := json.Marshal([]string{selectedID})
	if err != nil {
		t.Fatal(err)
	}
	admissionsJSON, err := json.Marshal([]WorkloadHandlerAdmission{{
		CapabilityID: workloadID, AuthorizedWorkloadID: "workload-1",
		RequiredRuntimeCapabilities: []string{"runtime.metadata.read-admin.v1"},
	}})
	if err != nil {
		t.Fatal(err)
	}

	for _, test := range []struct {
		name         string
		capabilityID string
		wantErr      error
	}{
		{name: "selected Catalog handler may rotate", capabilityID: selectedID},
		{name: "workload-only handler cannot enter Catalog", capabilityID: workloadID, wantErr: ErrConflict},
	} {
		t.Run(test.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatal(err)
			}
			defer db.Close()
			mock.ExpectBegin()
			mock.ExpectQuery(`(?s)SELECT OperationId FROM _System_CatalogAppOperation.*State = 'ACTIVE'.*Step = 'ACTIVATED' FOR UPDATE`).
				WithArgs(installation.ID, installation.ActiveRelease, "7").
				WillReturnRows(sqlmock.NewRows([]string{"OperationId"}).AddRow(operationID))
			mock.ExpectQuery(`(?s)SELECT SelectedHandlerCapabilities, WorkloadHandlerAdmissions FROM _System_CatalogAppOperation.*FOR UPDATE`).
				WithArgs(operationID).
				WillReturnRows(sqlmock.NewRows([]string{"SelectedHandlerCapabilities", "WorkloadHandlerAdmissions"}).
					AddRow(selectedJSON, admissionsJSON))
			mock.ExpectQuery(`(?s)SELECT CapabilityId, RequiredRuntimeCapabilities FROM _System_CatalogAppReleaseCapability.*ORDER BY CapabilityId$`).
				WithArgs(installation.AppID, installation.ActiveRelease, appauthority.CapabilityKindServiceHandler).
				WillReturnRows(sqlmock.NewRows([]string{"CapabilityId", "RequiredRuntimeCapabilities"}).
					AddRow(selectedID, `["runtime.records.read.v1"]`).
					AddRow(workloadID, `["runtime.metadata.read-admin.v1"]`))
			mock.ExpectRollback()
			tx, err := db.BeginTx(context.Background(), nil)
			if err != nil {
				t.Fatal(err)
			}
			err = validateActiveHandlerProjectionCapabilityTx(
				context.Background(), tx, installation, test.capabilityID,
			)
			if !errors.Is(err, test.wantErr) {
				t.Fatalf("active projection validation = %v, want %v", err, test.wantErr)
			}
			if err := tx.Rollback(); err != nil {
				t.Fatal(err)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestActiveHandlerProjectionPreservesPreGrammarAllHandlerInstallations(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	installation := installationRecord{
		ID: "00000000-0000-0000-0000-000000000001", AppID: "io.example.app",
		ActiveRelease: testDigest("release"), CatalogRevision: 3,
	}
	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)SELECT OperationId FROM _System_CatalogAppOperation.*FOR UPDATE`).
		WithArgs(installation.ID, installation.ActiveRelease, "3").
		WillReturnRows(sqlmock.NewRows([]string{"OperationId"}))
	mock.ExpectRollback()
	tx, err := db.BeginTx(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}
	if err := validateActiveHandlerProjectionCapabilityTx(
		context.Background(), tx, installation, "app.legacy-handler.v1",
	); err != nil {
		t.Fatalf("pre-grammar all-handler projection = %v", err)
	}
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestActiveHandlerProjectionPropagatesOperationEnumerationFailure(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	installation := installationRecord{
		ID: "00000000-0000-0000-0000-000000000001", AppID: "io.example.app",
		ActiveRelease: testDigest("release"), CatalogRevision: 3,
	}
	want := errors.New("operation enumeration failed")
	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)SELECT OperationId FROM _System_CatalogAppOperation.*FOR UPDATE`).
		WithArgs(installation.ID, installation.ActiveRelease, "3").
		WillReturnRows(sqlmock.NewRows([]string{"OperationId"}).
			AddRow("00000000-0000-0000-0000-000000000099").RowError(0, want))
	mock.ExpectRollback()
	tx, err := db.BeginTx(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}
	if err := validateActiveHandlerProjectionCapabilityTx(
		context.Background(), tx, installation, "app.handler.v1",
	); !errors.Is(err, want) {
		t.Fatalf("active operation enumeration failure = %v, want %v", err, want)
	}
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestHandlerProjectionPropagatesCapabilityEnumerationFailure(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	want := errors.New("capability enumeration failed")
	const operationID = "00000000-0000-0000-0000-000000000099"
	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)SELECT SelectedHandlerCapabilities, WorkloadHandlerAdmissions FROM _System_CatalogAppOperation.*FOR UPDATE`).
		WithArgs(operationID).
		WillReturnRows(sqlmock.NewRows([]string{"SelectedHandlerCapabilities", "WorkloadHandlerAdmissions"}).
			AddRow(nil, nil))
	mock.ExpectQuery(`(?s)SELECT CapabilityId, RequiredRuntimeCapabilities FROM _System_CatalogAppReleaseCapability.*ORDER BY CapabilityId$`).
		WithArgs("io.example.app", testDigest("release"), appauthority.CapabilityKindServiceHandler).
		WillReturnRows(sqlmock.NewRows([]string{"CapabilityId", "RequiredRuntimeCapabilities"}).
			AddRow("app.handler.v1", `[]`).RowError(0, want))
	mock.ExpectRollback()
	tx, err := db.BeginTx(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := operationHandlerProjectionCapabilitiesTx(
		context.Background(), tx, operationID, "io.example.app",
		testDigest("release"), nil, nil, "",
	); !errors.Is(err, want) {
		t.Fatalf("handler capability enumeration failure = %v, want %v", err, want)
	}
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestServiceHandlerCapabilityEnumerationDoesNotLockImmutableReleaseRows(t *testing.T) {
	if strings.Contains(strings.ToUpper(serviceHandlerCapabilitiesQuery), "FOR UPDATE") {
		t.Fatal("immutable AppRelease capability enumeration must not acquire row locks")
	}
}
