package catalogapp

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

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	consumerv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	layoutauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
	pagelayoutresolutionv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/pagelayoutresolutionv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
)

func TestCanonicalConsumerGrantsClosesCatalogCapabilitiesPerWorkload(t *testing.T) {
	values := []ConsumerGrant{
		{
			AuthorizedWorkloadID: "workload-sfcrm",
			CapabilityID:         consumerv1.AppMetadataCapabilityName,
		},
		{
			AuthorizedWorkloadID: "workload-integration",
			CapabilityID:         consumerv1.IntegrationBindingCapabilityName,
		},
	}
	got, err := canonicalConsumerGrants(
		[]ConsumerGrant{values[0], values[1]},
		[]string{"workload-integration", "workload-sfcrm"},
	)
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 2 ||
		got[0].AuthorizedWorkloadID != "workload-integration" ||
		got[1].AuthorizedWorkloadID != "workload-sfcrm" {
		t.Fatalf("canonical grants = %#v", got)
	}
	if &got[0] == &values[0] {
		t.Fatal("canonicalization retained the caller slice")
	}
}

func TestLayoutAuthoringConsumerGrantIsDerivedFromHandlerDependency(t *testing.T) {
	grants, err := canonicalOperationConsumerGrants(
		[]appauthority.ProvidedCapability{{
			CapabilityID: "app.layout-authoring.v1",
			Kind:         appauthority.CapabilityKindServiceHandler,
			HandlerRef:   "io.example.handler.layout-authoring.v1",
			RequiredRuntimeCapabilities: []string{
				layoutauthoringv1.CapabilityName,
			},
		}},
		[]string{layoutauthoringv1.CapabilityName},
		"runtime-layout-worker",
		nil,
		nil,
		[]string{"runtime-layout-worker"},
	)
	if err != nil {
		t.Fatalf("derive Layout authoring consumer grant: %v", err)
	}
	if len(grants) != 1 || grants[0] != (ConsumerGrant{
		AuthorizedWorkloadID: "runtime-layout-worker",
		CapabilityID:         layoutauthoringv1.CapabilityName,
	}) {
		t.Fatalf("derived Layout authoring consumer grants = %#v", grants)
	}
}

func TestCanonicalConsumerGrantsFailsClosed(t *testing.T) {
	valid := ConsumerGrant{
		AuthorizedWorkloadID: "workload-sfcrm",
		CapabilityID:         consumerv1.AppMetadataCapabilityName,
	}
	for _, test := range []struct {
		name   string
		values []ConsumerGrant
	}{
		{
			name: "unauthorized workload",
			values: []ConsumerGrant{{
				AuthorizedWorkloadID: "workload-other",
				CapabilityID:         consumerv1.AppMetadataCapabilityName,
			}},
		},
		{
			name: "unknown capability",
			values: []ConsumerGrant{{
				AuthorizedWorkloadID: valid.AuthorizedWorkloadID,
				CapabilityID:         "catalog.consumer.unknown.v1",
			}},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			if _, err := canonicalConsumerGrants(
				test.values, []string{valid.AuthorizedWorkloadID},
			); err == nil {
				t.Fatal("invalid consumer grant set was accepted")
			}
		})
	}
	got, err := canonicalConsumerGrants(
		[]ConsumerGrant{valid, valid}, []string{valid.AuthorizedWorkloadID},
	)
	if err != nil || len(got) != 1 || got[0] != valid {
		t.Fatalf("duplicate grant canonicalization = %#v, %v", got, err)
	}
}

func TestCanonicalConsumerGrantsRejectsMixedAppMetadataVersions(t *testing.T) {
	workloads := []string{"workload-sfcrm-old", "workload-sfcrm-new"}
	_, err := canonicalConsumerGrants([]ConsumerGrant{
		{
			AuthorizedWorkloadID: workloads[0],
			CapabilityID:         consumerv1.AppMetadataV2CapabilityName,
		},
		{
			AuthorizedWorkloadID: workloads[1],
			CapabilityID:         consumerv1.AppMetadataV3CapabilityName,
		},
	}, workloads)
	if err == nil {
		t.Fatal("mixed App metadata V2/V3 grants were accepted")
	}
}

func TestCanonicalConsumerGrantsAcceptsOneAppMetadataVersion(t *testing.T) {
	for _, capability := range []string{
		consumerv1.AppMetadataV2CapabilityName,
		consumerv1.AppMetadataV3CapabilityName,
	} {
		t.Run(capability, func(t *testing.T) {
			if _, err := canonicalConsumerGrants([]ConsumerGrant{{
				AuthorizedWorkloadID: "workload-sfcrm",
				CapabilityID:         capability,
			}}, []string{"workload-sfcrm"}); err != nil {
				t.Fatalf("single-version consumer grant rejected: %v", err)
			}
		})
	}
}

func TestCanonicalConsumerGrantsAcceptsPageLayoutResolution(t *testing.T) {
	const workloadID = "workload-sfcrm"
	_, err := canonicalConsumerGrants([]ConsumerGrant{{
		AuthorizedWorkloadID: workloadID,
		CapabilityID:         pagelayoutresolutionv1.CapabilityName,
	}}, []string{workloadID})
	if err != nil {
		t.Fatalf("PageLayout resolution consumer grant rejected: %v", err)
	}
}

func TestValidateBeginOperationDerivesCanonicalHandlerConsumerGrants(t *testing.T) {
	release := testReleaseWithHandlerConsumerDependencies(t)
	request := BeginOperationRequest{
		Kind:                   OperationPrepare,
		OrgID:                  "org-test",
		AppID:                  release.AppID,
		TargetAppReleaseDigest: release.AppReleaseDigest,
		IdempotencyKey:         "derive-handler-grants",
		RuntimeAudience:        "runtime-test",
		AuthorizedWorkloads:    []string{"workload-integration", "workload-sfcrm"},
		GrantedCapabilities:    slices.Clone(release.Package.Manifest.RequiredEngineCapabilities),
		HandlerWorkloadID:      "workload-sfcrm",
		ConsumerGrants: []ConsumerGrant{
			{
				AuthorizedWorkloadID: "workload-integration",
				CapabilityID:         consumerv1.IntegrationBindingCapabilityName,
			},
			// This migration-window duplicate must converge with the grant
			// derived from the verified handler dependency graph.
			{
				AuthorizedWorkloadID: "workload-sfcrm",
				CapabilityID:         consumerv1.AppMetadataV2CapabilityName,
			},
		},
	}
	authority := &Authority{}
	normalized, _, digest, err := authority.validateBeginOperationForTarget(
		context.Background(), request, &release, false,
	)
	if err != nil {
		t.Fatal(err)
	}
	want := []ConsumerGrant{
		{
			AuthorizedWorkloadID: "workload-integration",
			CapabilityID:         consumerv1.IntegrationBindingCapabilityName,
		},
		{
			AuthorizedWorkloadID: "workload-sfcrm",
			CapabilityID:         consumerv1.AppMetadataV2CapabilityName,
		},
		{
			AuthorizedWorkloadID: "workload-sfcrm",
			CapabilityID:         pagelayoutresolutionv1.CapabilityName,
		},
	}
	if !slices.Equal(normalized.ConsumerGrants, want) {
		t.Fatalf("canonical grants = %#v, want %#v", normalized.ConsumerGrants, want)
	}
	if slices.ContainsFunc(normalized.ConsumerGrants, func(value ConsumerGrant) bool {
		return value.CapabilityID == queryv1.CapabilityName
	}) {
		t.Fatalf("non-Catalog Runtime dependency became a Catalog grant: %#v", normalized.ConsumerGrants)
	}

	request.ConsumerGrants = request.ConsumerGrants[:1]
	replayed, _, replayDigest, err := authority.validateBeginOperationForTarget(
		context.Background(), request, &release, false,
	)
	if err != nil {
		t.Fatal(err)
	}
	if digest != replayDigest || !slices.Equal(normalized.ConsumerGrants, replayed.ConsumerGrants) {
		t.Fatalf("retry identity/grants changed: %s/%s, %#v/%#v", digest, replayDigest, normalized.ConsumerGrants, replayed.ConsumerGrants)
	}
	legacyNormalized := normalized
	legacyNormalized.HandlerWorkloadID = ""
	legacyDigest, err := operationRequestDigest(legacyNormalized)
	if err != nil {
		t.Fatal(err)
	}
	if digest != legacyDigest {
		t.Fatalf("pre-handler-workload operation identity changed: %s/%s", digest, legacyDigest)
	}
}

func TestCanonicalOperationConsumerGrantsUseEachHandlerOwner(t *testing.T) {
	provided := []appauthority.ProvidedCapability{
		{
			CapabilityID: "app.selected.v1", Kind: appauthority.CapabilityKindServiceHandler,
			RequiredRuntimeCapabilities: []string{consumerv1.AppMetadataV2CapabilityName},
		},
		{
			CapabilityID: "app.admitted.v1", Kind: appauthority.CapabilityKindServiceHandler,
			RequiredRuntimeCapabilities: []string{pagelayoutresolutionv1.CapabilityName},
		},
	}
	got, err := canonicalOperationConsumerGrants(
		provided,
		[]string{consumerv1.AppMetadataV2CapabilityName, pagelayoutresolutionv1.CapabilityName},
		"workload-selected",
		[]WorkloadHandlerAdmission{{
			CapabilityID: "app.admitted.v1", AuthorizedWorkloadID: "workload-admitted",
			RequiredRuntimeCapabilities: []string{pagelayoutresolutionv1.CapabilityName},
		}},
		nil,
		[]string{"workload-admitted", "workload-selected"},
	)
	if err != nil {
		t.Fatal(err)
	}
	want := []ConsumerGrant{
		{AuthorizedWorkloadID: "workload-admitted", CapabilityID: pagelayoutresolutionv1.CapabilityName},
		{AuthorizedWorkloadID: "workload-selected", CapabilityID: consumerv1.AppMetadataV2CapabilityName},
	}
	if !slices.Equal(got, want) {
		t.Fatalf("per-handler grants = %#v, want %#v", got, want)
	}
}

func TestValidateHandlerConsumerGrantAuthorityUsesActualBindingWorkload(t *testing.T) {
	required := []string{
		consumerv1.AppMetadataV2CapabilityName,
		queryv1.CapabilityName,
	}
	grants := []ConsumerGrant{{
		AuthorizedWorkloadID: "workload-a",
		CapabilityID:         consumerv1.AppMetadataV2CapabilityName,
	}}
	if err := validateHandlerConsumerGrantAuthority(required, "workload-a", grants); err != nil {
		t.Fatalf("granted handler workload rejected: %v", err)
	}
	if err := validateHandlerConsumerGrantAuthority(required, "workload-b", grants); !errors.Is(err, ErrConflict) {
		t.Fatalf("ungranted handler workload error = %v, want conflict", err)
	}
}

func TestValidateBeginOperationRejectsMalformedHandlerWorkloadBeforeIdentity(t *testing.T) {
	release := testReleaseWithHandlerConsumerDependencies(t)
	request := BeginOperationRequest{
		Kind:                   OperationPrepare,
		OrgID:                  "org-test",
		AppID:                  release.AppID,
		TargetAppReleaseDigest: release.AppReleaseDigest,
		IdempotencyKey:         "malformed-handler-workload",
		RuntimeAudience:        "runtime-test",
		AuthorizedWorkloads:    []string{"workload-sfcrm\x01"},
		GrantedCapabilities:    slices.Clone(release.Package.Manifest.RequiredEngineCapabilities),
		HandlerWorkloadID:      "workload-sfcrm\x01",
	}
	_, _, digest, err := (&Authority{}).validateBeginOperationForTarget(
		context.Background(), request, &release, false,
	)
	if err == nil || digest != "" {
		t.Fatalf("malformed workload result = digest %q, error %v", digest, err)
	}
	if !IsConsumerCapability(consumerv1.AppMetadataV2CapabilityName) ||
		!IsConsumerCapability(pagelayoutresolutionv1.CapabilityName) {
		t.Fatal("known Catalog capability classification changed with workload validation")
	}
}

func testReleaseWithHandlerConsumerDependencies(t *testing.T) RegisteredRelease {
	return testReleaseWithHandlerConsumerDependenciesForApp(
		t, "io.example.issues",
	)
}

func testReleaseWithHandlerConsumerDependenciesForApp(
	t *testing.T,
	appID string,
) RegisteredRelease {
	t.Helper()
	base := testRegisteredReleaseVersionForApp(t, appID, "1.0.0", "Subject")
	manifest := base.Package.Manifest
	manifest.RequiredEngineCapabilities = []string{
		consumerv1.AppMetadataV2CapabilityName,
		consumerv1.IntegrationBindingCapabilityName,
		pagelayoutresolutionv1.CapabilityName,
		queryv1.CapabilityName,
	}
	slices.Sort(manifest.RequiredEngineCapabilities)
	manifest.ProvidedCapabilities[0].RequiredRuntimeCapabilities = []string{
		consumerv1.AppMetadataV2CapabilityName,
		pagelayoutresolutionv1.CapabilityName,
		queryv1.CapabilityName,
	}
	slices.Sort(manifest.ProvidedCapabilities[0].RequiredRuntimeCapabilities)
	metadataDigest, err := appauthority.MetadataBundleDigest(base.Package.Bundle, manifest)
	if err != nil {
		t.Fatal(err)
	}
	manifest.MetadataBundleDigest = metadataDigest
	verified, err := appauthority.VerifyPackage(appauthority.Package{
		Manifest:       manifest,
		Bundle:         base.Package.Bundle,
		ComponentBlobs: base.Package.ComponentBlobs,
	})
	if err != nil {
		t.Fatal(err)
	}
	return registeredRelease(verified)
}

func TestConsumerGrantSetDigestCanonicalizesEmptySet(t *testing.T) {
	nilDigest, err := ComputeConsumerGrantSetDigest(nil)
	if err != nil {
		t.Fatal(err)
	}
	emptyDigest, err := ComputeConsumerGrantSetDigest([]ConsumerGrant{})
	if err != nil {
		t.Fatal(err)
	}
	if nilDigest != emptyDigest {
		t.Fatalf("nil/empty consumer grant digest = %s/%s", nilDigest, emptyDigest)
	}
}

func TestVerifyOperationConsumerGrantsUsesInstallationWorkloadAuthority(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	operation := Operation{
		OperationID:       "018f0000-0000-7000-9000-000000000001",
		Kind:              OperationPrepare,
		AppInstallationID: "018f0000-0000-7000-9000-000000000002",
		OrgID:             "org-test",
		AppID:             "io.nexuscrm.sfcrm",
	}
	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT
AuthorizedWorkloadId, CapabilityId, OrgId, AppInstallationId
FROM _System_CatalogConsumerGrant
WHERE OperationId = ?
ORDER BY BINARY AuthorizedWorkloadId, BINARY CapabilityId
FOR UPDATE`)).
		WithArgs(operation.OperationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"AuthorizedWorkloadId", "CapabilityId", "OrgId", "AppInstallationId",
		}).AddRow(
			"workload-integration",
			consumerv1.IntegrationBindingCapabilityName,
			operation.OrgID,
			operation.AppInstallationID,
		))
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT
OrgId, AppId, ActiveOperationId, CandidateAuthorizedWorkloads
FROM _System_CatalogAppInstallation
WHERE AppInstallationId = ?
FOR UPDATE`)).
		WithArgs(operation.AppInstallationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"OrgId", "AppId", "ActiveOperationId", "CandidateAuthorizedWorkloads",
		}).AddRow(
			operation.OrgID,
			operation.AppID,
			operation.OperationID,
			[]byte(`["workload-sfcrm"]`),
		))
	mock.ExpectRollback()
	tx, err := db.BeginTx(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}
	err = verifyOperationConsumerGrantsTx(context.Background(), tx, operation)
	if !errors.Is(err, ErrCorrupt) {
		t.Fatalf("self-authorizing staged grant error = %v, want ErrCorrupt", err)
	}
	_ = tx.Rollback()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadOperationConsumerGrantsReturnsCommittedSetAfterActivation(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)
	}
	operation := Operation{
		OperationID:              "018f0000-0000-7000-9000-000000000011",
		Kind:                     OperationPrepare,
		AppInstallationID:        "018f0000-0000-7000-9000-000000000012",
		OrgID:                    "org-test",
		AppID:                    "io.nexuscrm.sfcrm",
		State:                    StateActive,
		Step:                     "ACTIVATED",
		TargetAppReleaseDigest:   testDigest("release"),
		TargetCatalogRevision:    8,
		TargetSecurityEpoch:      9,
		ExpectedProjectionDigest: testDigest("projection"),
	}
	grants := []ConsumerGrant{{
		AuthorizedWorkloadID: "workload-sfcrm",
		CapabilityID:         pagelayoutresolutionv1.CapabilityName,
	}}
	grantDigest, err := ComputeConsumerGrantSetDigest(grants)
	if err != nil {
		t.Fatal(err)
	}
	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)SELECT OperationId, Kind, AppInstallationId, OrgId, AppId, State, Step,.*FROM _System_CatalogAppOperation WHERE OperationId = \?`).
		WithArgs(operation.OperationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"OperationId", "Kind", "AppInstallationId", "OrgId", "AppId", "State", "Step",
			"TargetReleaseDigest", "ExpectedCatalogRevision", "ExpectedSecurityEpoch", "ExpectedProjectionDigest",
			"AcknowledgedSecurityEpoch", "AcknowledgedProjectionDigest", "LastError",
		}).AddRow(
			operation.OperationID, operation.Kind, operation.AppInstallationID, operation.OrgID, operation.AppID,
			operation.State, operation.Step, operation.TargetAppReleaseDigest, []byte("8"), []byte("9"),
			operation.ExpectedProjectionDigest, []byte("9"), operation.ExpectedProjectionDigest, "",
		))
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT
AuthorizedWorkloadId, CapabilityId, OrgId, AppInstallationId
FROM _System_CatalogConsumerGrant
WHERE OperationId = ?
ORDER BY BINARY AuthorizedWorkloadId, BINARY CapabilityId
FOR UPDATE`)).
		WithArgs(operation.OperationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"AuthorizedWorkloadId", "CapabilityId", "OrgId", "AppInstallationId",
		}).AddRow(
			grants[0].AuthorizedWorkloadID, grants[0].CapabilityID,
			operation.OrgID, operation.AppInstallationID,
		))
	mock.ExpectQuery(`(?s)SELECT.*AppInstallationId, GrantSetDigest, GrantCount.*FROM _System_CatalogConsumerGrantSet.*FOR UPDATE`).
		WithArgs(operation.OperationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"AppInstallationId", "GrantSetDigest", "GrantCount",
		}).AddRow(operation.AppInstallationID, grantDigest, []byte("1")))
	mock.ExpectCommit()

	got, err := authority.ReadOperationConsumerGrants(context.Background(), operation.OperationID)
	if err != nil {
		t.Fatal(err)
	}
	if !slices.Equal(got, grants) {
		t.Fatalf("operation grants = %#v, want %#v", got, grants)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadOperationConsumerGrantsNormalizesLegacyEmptyCommitment(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)
	}
	operation := Operation{
		OperationID:            "018f0000-0000-7000-9000-000000000021",
		Kind:                   OperationPrepare,
		AppInstallationID:      "018f0000-0000-7000-9000-000000000022",
		OrgID:                  "org-test",
		AppID:                  "io.nexuscrm.sfcrm",
		State:                  StatePreparing,
		Step:                   "PROJECTION_ACKNOWLEDGED",
		TargetAppReleaseDigest: testDigest("legacy-release"),
		TargetCatalogRevision:  4,
		TargetSecurityEpoch:    5,
	}
	emptyDigest, err := ComputeConsumerGrantSetDigest(nil)
	if err != nil {
		t.Fatal(err)
	}
	mock.ExpectBegin()
	mock.ExpectQuery(`(?s)SELECT OperationId, Kind, AppInstallationId, OrgId, AppId, State, Step,.*FROM _System_CatalogAppOperation WHERE OperationId = \?`).
		WithArgs(operation.OperationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"OperationId", "Kind", "AppInstallationId", "OrgId", "AppId", "State", "Step",
			"TargetReleaseDigest", "ExpectedCatalogRevision", "ExpectedSecurityEpoch", "ExpectedProjectionDigest",
			"AcknowledgedSecurityEpoch", "AcknowledgedProjectionDigest", "LastError",
		}).AddRow(
			operation.OperationID, operation.Kind, operation.AppInstallationID, operation.OrgID, operation.AppID,
			operation.State, operation.Step, operation.TargetAppReleaseDigest, []byte("4"), []byte("5"),
			testDigest("projection"), []byte("5"), testDigest("projection"), "",
		))
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT
AuthorizedWorkloadId, CapabilityId, OrgId, AppInstallationId
FROM _System_CatalogConsumerGrant
WHERE OperationId = ?
ORDER BY BINARY AuthorizedWorkloadId, BINARY CapabilityId
FOR UPDATE`)).
		WithArgs(operation.OperationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"AuthorizedWorkloadId", "CapabilityId", "OrgId", "AppInstallationId",
		}))
	mock.ExpectQuery(`(?s)SELECT.*AppInstallationId, GrantSetDigest, GrantCount.*FROM _System_CatalogConsumerGrantSet.*FOR UPDATE`).
		WithArgs(operation.OperationID).
		WillReturnRows(sqlmock.NewRows([]string{"AppInstallationId", "GrantSetDigest", "GrantCount"}))
	mock.ExpectExec(`(?s)INSERT INTO _System_CatalogConsumerGrantSet.*VALUES \(\?, \?, \?, 0, CURRENT_TIMESTAMP\(6\)\)`).
		WithArgs(operation.OperationID, operation.AppInstallationID, emptyDigest).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	grants, err := authority.ReadOperationConsumerGrants(context.Background(), operation.OperationID)
	if err != nil {
		t.Fatal(err)
	}
	if len(grants) != 0 {
		t.Fatalf("legacy empty grants = %#v", grants)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}
