package catalogreconcile

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	database "github.com/nexuscrm/db"
	"github.com/nexuscrm/persistence"
)

func TestRepositoryMetadataReaderRejectsMissingAuthority(t *testing.T) {
	if _, err := NewRepositoryMetadataReader(nil); err == nil {
		t.Fatal("nil metadata repository was accepted")
	}
}

func TestRepositoryMetadataReaderDerivedFactsReuseCatalogTransaction(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	// If any derived reader ignores MetadataRepository.GetExecutor(ctx), it
	// waits for a second connection instead of observing this transaction.
	db.SetMaxOpenConns(1)

	mock.ExpectBegin()
	mock.ExpectQuery("FROM FlowDefinition fd").
		WillReturnRows(sqlmock.NewRows([]string{
			"definition_id", "developer_name", "definition_process_type", "active_version_id",
			"version_id", "version_definition_id", "version_number", "status", "description",
			"version_process_type", "metadata", "source", "is_deleted",
		}))
	mock.ExpectQuery("FROM `_System_Field` f").
		WillReturnRows(sqlmock.NewRows([]string{
			"field_id", "object_api_name", "field_api_name", "format", "scope", "start", "token_refs",
		}))
	mock.ExpectQuery("FROM _System_ObjectFieldMapping").
		WillReturnRows(sqlmock.NewRows([]string{
			"source_object", "target_object", "action_type", "source", "source_field", "target_field", "default_value", "value_map", "on_unmapped_value", "source_lookup_field",
		}))
	mock.ExpectQuery("FROM _System_ObjectFieldMapping").WithArgs("Issue").
		WillReturnRows(sqlmock.NewRows([]string{
			"source_object", "target_object", "action_type", "source_field", "target_field", "source_lookup_field",
		}))
	mock.ExpectQuery("FROM `_System_ValidationRule`").
		WillReturnRows(sqlmock.NewRows([]string{"unused"}))
	mock.ExpectQuery("FROM `_System_RecordType`").WithArgs("Issue", false).
		WillReturnRows(sqlmock.NewRows([]string{"unused"}))
	const (
		packName      = "pack-sfcrm"
		packInstallID = "pack-install-sfcrm"
		payloadDigest = "nexus-pack-sha256-v1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	)
	mock.ExpectQuery("(?s)SELECT PackName.*FROM _System_PackActivation.*FOR UPDATE").
		WillReturnRows(sqlmock.NewRows([]string{"PackName"}).AddRow(packName))
	mock.ExpectQuery("(?s)FROM _System_PackActivation.*PackName = \\?.*FOR UPDATE").
		WithArgs(packName).
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "PackName", "ActivePackInstallId",
			"PreviousPackInstallId", "ActivationEpoch", "ActivatedDate",
			"IsDeleted",
		}).AddRow(
			testPackAuthorityID("packact_", packName),
			packName,
			packInstallID,
			nil,
			[]byte("1"),
			time.Now(),
			0,
		))
	mock.ExpectQuery("(?s)FROM _System_PackGeneration.*PackInstallId = \\?.*FOR UPDATE").
		WithArgs(packInstallID).
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "PackInstallId", "PackName", "Category", "Status",
			"PackVersion", "ManifestFormatVersion", "PlatformCompatibility",
			"DesiredPayloadHash", "AppliedPayloadHash", "PayloadHashAlgorithm",
			"SourceKind", "SourceURI", "SourceRevision",
			"HostRequirementsDigest", "RuntimeObjectSpecDeclarations",
			"RuntimeRelatedListRules", "RuntimeSearchScopeOrder",
			"RuntimeSearchAllDefaultFields", "RuntimeFormulaFunctionBundles",
			"RuntimeStartupShallowDeclarations", "LastError", "IsDeleted",
		}).AddRow(
			testPackAuthorityID(
				"packgen_",
				packName+"\x00"+packInstallID,
			),
			packInstallID,
			packName,
			"app-pack",
			"active",
			"1.0.0",
			"1",
			nil,
			payloadDigest,
			payloadDigest,
			"nexus-pack-sha256-v1",
			"official_repo",
			packName,
			"source-revision-1",
			"nexus-hostreq-sha256-v1:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
			`[]`,
			`[]`,
			`[]`,
			`[{"objectApiName":"Issue","fields":["Name"]}]`,
			`[]`,
			`[]`,
			nil,
			0,
		))
	mock.ExpectCommit()

	repository := persistence.NewMetadataRepository(db)
	reader, err := NewRepositoryMetadataReader(repository)
	if err != nil {
		t.Fatal(err)
	}
	manager := persistence.NewTransactionManager(database.NewConnectionFromDB(db))
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := manager.WithTransaction(ctx, func(txCtx context.Context) error {
		if _, err := reader.GetActiveTriggerSubscriptions(txCtx); err != nil {
			return err
		}
		if _, err := reader.GetAutoNumberRules(txCtx); err != nil {
			return err
		}
		if _, err := reader.GetFieldMappingRules(txCtx); err != nil {
			return err
		}
		if _, err := reader.GetCreateFromSourceGuardEntries(txCtx, "Issue"); err != nil {
			return err
		}
		if _, err := reader.GetAllValidationRules(txCtx); err != nil {
			return err
		}
		if _, err := reader.GetRecordTypes(txCtx, "Issue"); err != nil {
			return err
		}
		declarations, err := reader.GetRuntimeSearchAllDefaultFields(txCtx)
		if err != nil {
			return err
		}
		if len(declarations) != 1 ||
			declarations[0].ObjectAPIName != "Issue" ||
			len(declarations[0].Fields) != 1 ||
			declarations[0].Fields[0] != "Name" {
			t.Fatalf("search-all declarations = %#v", declarations)
		}
		return nil
	}); err != nil {
		t.Fatalf("transaction-bound derived metadata reads: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("SQL expectations: %v", err)
	}
}

func TestRepositoryMetadataReaderPreservesCompleteFieldMappingPolicy(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()

	mock.ExpectQuery("FROM _System_ObjectFieldMapping").
		WillReturnRows(sqlmock.NewRows([]string{
			"source_object", "target_object", "action_type", "source", "source_field", "target_field",
			"default_value", "value_map", "on_unmapped_value", "source_lookup_field",
		}).AddRow(
			"Lead", "Account", "convertLead", "runtime", "LegacyStatus", "Status",
			"retired", `{"retired":"Current"}`, "passthrough", "ParentId",
		))

	repository := persistence.NewMetadataRepository(db)
	reader, err := NewRepositoryMetadataReader(repository)
	if err != nil {
		t.Fatal(err)
	}
	rules, err := reader.GetFieldMappingRules(t.Context())
	if err != nil {
		t.Fatal(err)
	}
	if len(rules) != 1 || rules[0].SourceObject != "Lead" || rules[0].TargetObject != "Account" ||
		rules[0].ActionType != "convertLead" || rules[0].SourceField != "LegacyStatus" ||
		rules[0].TargetField != "Status" || rules[0].DefaultValue != "retired" ||
		rules[0].ValueMap != `{"retired":"Current"}` || rules[0].OnUnmappedValue != "passthrough" ||
		rules[0].SourceLookupField != "ParentId" {
		t.Fatalf("field mapping policy = %#v", rules)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("SQL expectations: %v", err)
	}
}

func testPackAuthorityID(prefix string, value string) string {
	sum := sha256.Sum256([]byte(value))
	return prefix + hex.EncodeToString(sum[:])
}
