package catalogapp

import (
	"context"
	"errors"
	"sort"
	"strconv"
	"testing"

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

func TestCommitOperationMetadataSelectionIsImmutableAndIdempotent(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)
	}
	operationID := "01900000-0000-7000-8000-000000000021"
	installationID := "01900000-0000-7000-8000-000000000022"
	digest := testDigest("catalog-snapshot-a")
	changed := testDigest("catalog-snapshot-b")

	expectServingOperation(t, mock, operationID, installationID, "io.example.app", testDigest("release"), "CANDIDATE_COMMITTED")
	mock.ExpectQuery(`(?s)SELECT EvidenceDigest FROM _System_CatalogAppOperationJournal.*Step = \?.*FOR UPDATE`).
		WithArgs(operationID, operationJournalStepMetadataSelectionCommitted).
		WillReturnRows(sqlmock.NewRows([]string{"EvidenceDigest"}))
	mock.ExpectQuery(`(?s)SELECT SequenceNumber FROM _System_CatalogAppOperationJournal.*ORDER BY SequenceNumber DESC LIMIT 1 FOR UPDATE`).
		WithArgs(operationID).WillReturnRows(sqlmock.NewRows([]string{"SequenceNumber"}).AddRow("1"))
	mock.ExpectExec(`(?s)INSERT INTO _System_CatalogAppOperationJournal.*VALUES \(\?, \?, \?, \?, \?, CURRENT_TIMESTAMP\(6\)\)`).
		WithArgs(operationID, "2", operationJournalStepMetadataSelectionCommitted, string(StatePreparing), digest).
		WillReturnResult(sqlmock.NewResult(0, 1))
	expectNoOperationConsumerGrants(mock, operationID)
	mock.ExpectCommit()
	if got, err := authority.CommitOperationMetadataSelection(context.Background(), operationID, digest); err != nil || got != digest {
		t.Fatalf("first metadata selection = %q, %v", got, err)
	}
	mock.ExpectQuery(`(?s)SELECT EvidenceDigest FROM _System_CatalogAppOperationJournal.*Step = \?.*ORDER BY SequenceNumber`).
		WithArgs(operationID, operationJournalStepMetadataSelectionCommitted).
		WillReturnRows(sqlmock.NewRows([]string{"EvidenceDigest"}).AddRow(digest))
	if got, err := authority.ReadOperationMetadataSelection(context.Background(), operationID); err != nil || got != digest {
		t.Fatalf("read metadata selection = %q, %v", got, err)
	}

	expectServingOperation(t, mock, operationID, installationID, "io.example.app", testDigest("release"), "CANDIDATE_COMMITTED")
	mock.ExpectQuery(`(?s)SELECT EvidenceDigest FROM _System_CatalogAppOperationJournal.*Step = \?.*FOR UPDATE`).
		WithArgs(operationID, operationJournalStepMetadataSelectionCommitted).
		WillReturnRows(sqlmock.NewRows([]string{"EvidenceDigest"}).AddRow(digest))
	expectNoOperationConsumerGrants(mock, operationID)
	mock.ExpectCommit()
	if got, err := authority.CommitOperationMetadataSelection(context.Background(), operationID, digest); err != nil || got != digest {
		t.Fatalf("metadata selection retry = %q, %v", got, err)
	}

	expectServingOperation(t, mock, operationID, installationID, "io.example.app", testDigest("release"), "CANDIDATE_COMMITTED")
	mock.ExpectQuery(`(?s)SELECT EvidenceDigest FROM _System_CatalogAppOperationJournal.*Step = \?.*FOR UPDATE`).
		WithArgs(operationID, operationJournalStepMetadataSelectionCommitted).
		WillReturnRows(sqlmock.NewRows([]string{"EvidenceDigest"}).AddRow(digest))
	mock.ExpectRollback()
	if _, err := authority.CommitOperationMetadataSelection(context.Background(), operationID, changed); !errors.Is(err, ErrConflict) {
		t.Fatalf("changed metadata selection error = %v, want conflict", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestCommitOperationMetadataSelectionAtomicallyBindsV2Projection(
	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)
	}
	operationID := "01900000-0000-7000-8000-000000000031"
	installationID := "01900000-0000-7000-8000-000000000032"
	snapshotDigest := testDigest("catalog-snapshot-v2")
	components := testRequiredEffectiveMetadataComponents()
	component := components[0]
	objectSearchComponent := components[1]
	setDigest, err := ComputeEffectiveMetadataSetDigest(components)
	if err != nil {
		t.Fatal(err)
	}

	expectServingOperation(
		t, mock, operationID, installationID, "io.example.app",
		testDigest("release"), "CANDIDATE_COMMITTED",
	)
	mock.ExpectQuery(
		`(?s)SELECT EvidenceDigest FROM _System_CatalogAppOperationJournal.*Step = \?.*FOR UPDATE`,
	).WithArgs(
		operationID, operationJournalStepMetadataSelectionCommitted,
	).WillReturnRows(sqlmock.NewRows([]string{"EvidenceDigest"}))
	mock.ExpectQuery(
		`(?s)SELECT SequenceNumber FROM _System_CatalogAppOperationJournal.*ORDER BY SequenceNumber DESC LIMIT 1 FOR UPDATE`,
	).WithArgs(operationID).WillReturnRows(
		sqlmock.NewRows([]string{"SequenceNumber"}).AddRow("1"),
	)
	mock.ExpectExec(
		`(?s)INSERT INTO _System_CatalogAppOperationJournal.*VALUES \(\?, \?, \?, \?, \?, CURRENT_TIMESTAMP\(6\)\)`,
	).WithArgs(
		operationID, "2", operationJournalStepMetadataSelectionCommitted,
		string(StatePreparing), snapshotDigest,
	).WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectQuery(
		`(?s)SELECT.*FROM _System_CatalogConsumerGrant.*FOR UPDATE`,
	).WithArgs(operationID).WillReturnRows(sqlmock.NewRows([]string{
		"AuthorizedWorkloadId", "CapabilityId", "OrgId",
		"AppInstallationId",
	}).AddRow(
		"workload-sfcrm",
		consumerv1.AppMetadataV2CapabilityName,
		"org-test",
		installationID,
	))
	for _, value := range components {
		mock.ExpectExec(
			`(?s)INSERT INTO _System_CatalogEffectiveMetadataComponent`,
		).WithArgs(
			sqlmock.AnyArg(), operationID, "org-test", installationID,
			value.ComponentKind,
			strconv.FormatUint(uint64(value.ComponentFormatVersion), 10),
			value.BusinessKey,
			value.ContentDigest, value.CanonicalBehavior,
		).WillReturnResult(sqlmock.NewResult(0, 1))
	}
	mock.ExpectExec(
		`(?s)INSERT INTO _System_CatalogEffectiveMetadataSet`,
	).WithArgs(
		operationID, "org-test", installationID, snapshotDigest,
		setDigest, "2",
	).WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	got, err := authority.
		CommitOperationMetadataSelectionWithEffectiveComponents(
			context.Background(), operationID, snapshotDigest,
			[]EffectiveMetadataComponent{objectSearchComponent, component},
		)
	if err != nil || got != snapshotDigest {
		t.Fatalf("v2 metadata selection = %q, %v", got, err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func testRequiredEffectiveMetadataComponents() []EffectiveMetadataComponent {
	applicationNavigation := EffectiveMetadataComponent{
		ComponentKind:          consumerv1.EffectiveApplicationNavigationComponentKind,
		ComponentFormatVersion: 1,
		BusinessKey:            consumerv1.EffectiveApplicationNavigationComponentBusinessKey,
		CanonicalBehavior:      []byte(`{"applications":[]}`),
	}
	applicationNavigation.ContentDigest =
		consumerv1.ComputeEffectiveMetadataComponentDigest(
			applicationNavigation.CanonicalBehavior,
		)
	objectSearch := EffectiveMetadataComponent{
		ComponentKind:          consumerv1.EffectiveObjectSearchComponentKind,
		ComponentFormatVersion: 2,
		BusinessKey:            consumerv1.EffectiveObjectSearchComponentBusinessKey,
		CanonicalBehavior:      []byte(`{"objects":[]}`),
	}
	objectSearch.ContentDigest =
		consumerv1.ComputeEffectiveMetadataComponentDigest(
			objectSearch.CanonicalBehavior,
		)
	return []EffectiveMetadataComponent{applicationNavigation, objectSearch}
}

func expectNoOperationConsumerGrants(mock sqlmock.Sqlmock, operationID string) {
	mock.ExpectQuery(`(?s)SELECT.*FROM _System_CatalogConsumerGrant.*FOR UPDATE`).
		WithArgs(operationID).
		WillReturnRows(sqlmock.NewRows([]string{
			"AuthorizedWorkloadId", "CapabilityId", "OrgId",
			"AppInstallationId",
		}))
}

func TestRequiredEffectiveMetadataSetRejectsMissingComponent(t *testing.T) {
	if err := validateRequiredEffectiveMetadataComponents(
		nil, consumerv1.AppMetadataV2CapabilityName,
	); !errors.Is(err, ErrNotReady) {
		t.Fatalf("missing required effective metadata component error = %v, want not ready", err)
	}
}

func TestRequiredEffectiveMetadataSetAcceptsHistoricalObjectSearchFormat(t *testing.T) {
	for _, format := range []uint32{1, 2, 3, 4, 5} {
		components := testRequiredEffectiveMetadataComponents()
		components[1].ComponentFormatVersion = format
		if err := validateRequiredEffectiveMetadataComponents(
			components, consumerv1.AppMetadataV3CapabilityName,
		); err != nil {
			t.Fatalf("historical object-search format %d error = %v", format, err)
		}
	}
}

func TestCurrentRequiredEffectiveMetadataSetRejectsHistoricalObjectSearchFormat(t *testing.T) {
	components := testRequiredEffectiveMetadataComponents()
	components[1].ComponentFormatVersion = 1
	if err := validateCurrentRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV3CapabilityName,
	); !errors.Is(err, ErrNotReady) {
		t.Fatalf("new-write object-search format 1 error = %v, want not ready", err)
	}
}

func TestCurrentRequiredEffectiveMetadataSetBindsObjectSearchFormatToCapability(t *testing.T) {
	components := testRequiredEffectiveMetadataComponents()
	if err := validateCurrentRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV2CapabilityName,
	); err != nil {
		t.Fatalf("V2 current object-search format 2 error = %v", err)
	}
	if err := validateCurrentRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV3CapabilityName,
	); !errors.Is(err, ErrNotReady) {
		t.Fatalf("V3 accepted current object-search format 2: %v", err)
	}
	components[1].ComponentFormatVersion = 6
	if err := validateCurrentRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV3CapabilityName,
	); err != nil {
		t.Fatalf("V3 current object-search format 6 error = %v", err)
	}
}

func TestRequiredEffectiveMetadataSetRejectsUnknownObjectSearchFormat(t *testing.T) {
	components := testRequiredEffectiveMetadataComponents()
	components[1].ComponentFormatVersion = 7
	if err := validateRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV3CapabilityName,
	); !errors.Is(err, ErrNotReady) {
		t.Fatalf("object-search format 7 error = %v, want not ready", err)
	}
}

func TestEffectivePageLayoutIsV3Only(t *testing.T) {
	components := testRequiredEffectiveMetadataComponents()
	layout := EffectiveMetadataComponent{
		ComponentKind:          consumerv1.EffectivePageLayoutComponentKind,
		ComponentFormatVersion: 1,
		BusinessKey:            "Account/default",
		CanonicalBehavior:      []byte(`{"objectApiName":"Account"}`),
	}
	layout.ContentDigest = consumerv1.ComputeEffectiveMetadataComponentDigest(layout.CanonicalBehavior)
	components = append(components, layout)
	if err := validateRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV2CapabilityName,
	); !errors.Is(err, ErrNotReady) {
		t.Fatalf("V2 page-layout component error = %v, want not ready", err)
	}
	if err := validateRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV3CapabilityName,
	); err != nil {
		t.Fatalf("V3 page-layout component error = %v", err)
	}
}

func TestEffectiveMetadataV3AcceptsOpaqueComponentBetweenRequiredKinds(t *testing.T) {
	components := testRequiredEffectiveMetadataComponents()
	opaque := EffectiveMetadataComponent{
		ComponentKind:          "metadata.effective.object-action.v1",
		ComponentFormatVersion: 1,
		BusinessKey:            "Contract",
		CanonicalBehavior:      []byte(`[{"name":"ChangeOwner"}]`),
	}
	opaque.ContentDigest =
		consumerv1.ComputeEffectiveMetadataComponentDigest(opaque.CanonicalBehavior)
	components = append(components, opaque)
	sort.Slice(components, func(left, right int) bool {
		return effectiveMetadataComponentKey(components[left]) <
			effectiveMetadataComponentKey(components[right])
	})

	if err := validateRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV3CapabilityName,
	); err != nil {
		t.Fatalf("V3 rejected an opaque effective component: %v", err)
	}
	if err := validateRequiredEffectiveMetadataComponents(
		components, consumerv1.AppMetadataV2CapabilityName,
	); !errors.Is(err, ErrNotReady) {
		t.Fatalf("V2 opaque effective component error = %v, want not ready", err)
	}
}
