package controlplane

import (
	"context"
	"database/sql"
	"go/ast"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

func TestRunnerOwnedGlobalRuntimeConsumersUseRunnerRegistrationSet(t *testing.T) {
	for _, test := range []struct {
		file string
		name string
	}{
		{file: "operations.go", name: "rebuildDerived"},
		{file: "source_pack_ops.go", name: "restoreInstalledRuntimeMetadataAfterInstallFailure"},
	} {
		t.Run(test.name, func(t *testing.T) {
			fn := parseControlplaneFuncDecl(t, test.file, test.name)
			call := findFuncCallByName(fn, "RegisterInstalledRuntimeMetadataWithComponentKindRegistrationSet")
			if call == nil {
				t.Fatalf("%s must use the registration-aware installed runtime carrier", test.name)
			}
			if len(call.Args) != 3 || !isRunnerComponentRegistrationSetCall(call.Args[2]) {
				t.Fatalf("%s must pass r.componentKindRegistrationSet() to the installed runtime carrier", test.name)
			}
		})
	}
}

func TestRebuildDerivedWithLockRunnerOptionsOwnSourceBackedConsumers(t *testing.T) {
	legacy := parseControlplaneFuncDecl(t, "operations.go", "RebuildDerivedWithLock")
	delegation := findFuncCallByName(legacy, "RebuildDerivedWithLockAndRunnerOptions")
	if delegation == nil {
		t.Fatal("RebuildDerivedWithLock must delegate to the RunnerOptions variant")
	}
	if len(delegation.Args) != 3 || !isEmptyRunnerOptionsLiteral(delegation.Args[2]) {
		t.Fatal("RebuildDerivedWithLock must preserve compatibility with empty RunnerOptions")
	}

	variant := parseControlplaneFuncDecl(t, "operations.go", "RebuildDerivedWithLockAndRunnerOptions")
	runnerCall := findFuncCallByName(variant, "NewRunnerWithOptions")
	if runnerCall == nil || len(runnerCall.Args) != 2 || !isIdentifier(runnerCall.Args[1], "runnerOptions") {
		t.Fatal("RebuildDerivedWithLockAndRunnerOptions must construct the runner from its RunnerOptions snapshot")
	}
	if call := findFuncCallByName(variant, "NewRunner"); call != nil {
		t.Fatal("RebuildDerivedWithLockAndRunnerOptions must not discard RunnerOptions through NewRunner")
	}
	// The lock-holding variant calls rebuildDerived directly (not the
	// exported RebuildDerived) so it can enable receipt settlement, but the
	// invariant is unchanged: the rebuild must receive the runner built from
	// this function's RunnerOptions snapshot.
	rebuildCall := findFuncCallByName(variant, "rebuildDerived")
	if rebuildCall == nil || len(rebuildCall.Args) != 4 || !isIdentifier(rebuildCall.Args[1], "r") {
		t.Fatal("RebuildDerivedWithLockAndRunnerOptions must pass its configured runner to rebuildDerived")
	}
}

func TestVerifyRunnerOptionsOwnEverySourceBackedRead(t *testing.T) {
	for _, test := range []struct {
		name string
		call string
	}{
		{name: "Verify", call: "VerifyWithRunnerOptions"},
		{name: "VerifyDeep", call: "VerifyDeepWithRunnerOptions"},
	} {
		fn := parseControlplaneFuncDecl(t, "verify.go", test.name)
		if call := findFuncCallByName(fn, test.call); call == nil {
			t.Fatalf("%s must call %s", test.name, test.call)
		}
	}
	verifyImpl := parseControlplaneFuncDecl(t, "verify.go", "verifyImpl")
	registerCall := findFuncCallByName(verifyImpl, "registerInstalledRuntimeMetadataForVerify")
	if registerCall == nil || len(registerCall.Args) != 4 || !isSelector(registerCall.Args[3], "options", "ComponentKindRegistrationSet") {
		t.Fatal("verifyImpl must pass RunnerOptions registrations into deep runtime registration")
	}
	runnerCall := findFuncCallByName(verifyImpl, "NewRunnerWithOptions")
	if runnerCall == nil || len(runnerCall.Args) != 2 || !isIdentifier(runnerCall.Args[1], "options") {
		t.Fatal("verifyImpl must construct the verify runner from the same RunnerOptions snapshot")
	}
	registerVerify := parseControlplaneFuncDecl(t, "verify.go", "registerInstalledRuntimeMetadataForVerify")
	carrierCall := findFuncCallByName(registerVerify, "RegisterInstalledRuntimeMetadataWithComponentKindRegistrationSet")
	if carrierCall == nil || len(carrierCall.Args) != 3 || !isIdentifier(carrierCall.Args[2], "registrations") {
		t.Fatal("deep verify must pass its registration snapshot into the global runtime carrier")
	}

	verifyInstalled := parseControlplaneFuncDecl(t, "verify.go", "verifyInstalledAppPacks")
	if findFuncCallByName(verifyInstalled, "validatePackManagedApprovalOwnership") == nil {
		t.Fatal("global verify is missing the managed approval coverage guard")
	}
	call := findFuncCallByName(verifyInstalled, "installedAppPackForRowWithLazyFormulaBundles")
	if call == nil || len(call.Args) != 5 || !isIdentifier(call.Args[4], "registrations") {
		t.Fatal("verifyInstalledAppPacks must resolve every installed source with runner registrations")
	}
	shallowCall := findFuncCallByName(verifyInstalled, "VerifyInstalledPersistedRuntimeShallowByNameWithComponentKindRegistrationSet")
	if shallowCall == nil || len(shallowCall.Args) != 5 || !isIdentifier(shallowCall.Args[4], "registrations") {
		t.Fatal("shallow readiness must pass runner registrations into nested source fallbacks")
	}
	verifyByName := parseControlplaneFuncDecl(t, "source_pack_ops.go", "verifyInstalledPackByName")
	coverage := findFuncCallByName(verifyByName, "validatePackManagedApprovalOwnership")
	resolve := findFuncCallByName(verifyByName, "ResolveInstalledPack")
	if coverage == nil || resolve == nil || coverage.Pos() > resolve.Pos() {
		t.Fatal("per-pack verify must check managed approval coverage before source resolution")
	}
	compileProof := findFuncCallByName(verifyByName, "formulaViewCompileProofForDBWithRunnerOptions")
	if compileProof == nil || len(compileProof.Args) != 2 || !isIdentifier(compileProof.Args[0], "db") || !isIdentifier(compileProof.Args[1], "options") {
		t.Fatal("per-pack named verify must prepare formula compile proof from the same RunnerOptions snapshot")
	}
}

func TestRegisterInstalledRuntimeMetadataForVerifyUsesRegistrationSet(t *testing.T) {
	fixture := newRegisteredRuntimeConsumerFixture(t)
	t.Cleanup(func() {
		if err := packs.RegisterRuntimeMetadataForPacks(context.Background(), nil, nil); err != nil {
			t.Errorf("reset runtime metadata: %v", err)
		}
	})

	for _, test := range []struct {
		name          string
		registrations packs.ComponentKindRegistrationSet
		wantError     bool
	}{
		{name: "registered", registrations: fixture.options.ComponentKindRegistrationSet},
		{name: "missing registration", wantError: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatal(err)
			}
			defer db.Close()
			fixture.expectActiveRuntimeRow(mock)

			err = registerInstalledRuntimeMetadataForVerify(context.Background(), db, true, test.registrations)
			if test.wantError {
				if err == nil || !strings.Contains(err.Error(), "runner_managed_test") {
					t.Fatalf("missing registration error = %v", err)
				}
			} else if err != nil {
				t.Fatalf("registered deep runtime metadata: %v", err)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestRefreshPersistedRuntimeDeclarationsForActivePacksOwnsRegisteredResolution(t *testing.T) {
	fixture := newRegisteredRuntimeConsumerFixture(t)

	for _, test := range []struct {
		name          string
		registrations packs.ComponentKindRegistrationSet
		legacyWrapper bool
		wantError     bool
	}{
		{name: "registered", registrations: fixture.options.ComponentKindRegistrationSet},
		{name: "empty legacy wrapper", legacyWrapper: true, wantError: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatal(err)
			}
			defer db.Close()
			expectRegisteredPersistedRuntimeDeclarationRefresh(mock, fixture)

			if test.legacyWrapper {
				err = refreshPersistedRuntimeDeclarationsForActivePacks(context.Background(), db)
			} else {
				err = refreshPersistedRuntimeDeclarationsForActivePacksWithComponentKindRegistrationSet(
					context.Background(), db, test.registrations,
				)
			}
			if test.wantError {
				if err == nil || !strings.Contains(err.Error(), "runner_managed_test") {
					t.Fatalf("missing registration error = %v", err)
				}
			} else if err != nil {
				t.Fatalf("refresh registered persisted declarations: %v", err)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func expectRegisteredPersistedRuntimeDeclarationRefresh(mock sqlmock.Sqlmock, fixture registeredRuntimeConsumerFixture) {
	expectInstalledPackRuntimeDeclarationColumns(mock, true)
	expectInstalledPackRuntimeStartupShallowAndFormulaBundleDeclarationColumns(mock, true, true)
	expectInstalledPackRuntimeDeclarationColumns(mock, true)
	expectInstalledPackRuntimeStartupShallowAndFormulaBundleDeclarationColumns(mock, true, true)
	metadata := packs.PackHashMetadataForPack(fixture.pack)
	mock.ExpectQuery("SELECT Name, Category, Status, PackVersion, ManifestFormatVersion, PlatformCompatibility, DesiredPayloadHash, AppliedPayloadHash, PayloadHashAlgorithm, SourceKind, SourceURI, SourceRevision").
		WithArgs(string(packs.AppPack), string(packs.ConnectorPack)).
		WillReturnRows(sqlmock.NewRows([]string{
			"Name", "Category", "Status", "PackVersion", "ManifestFormatVersion", "PlatformCompatibility",
			"DesiredPayloadHash", "AppliedPayloadHash", "PayloadHashAlgorithm", "SourceKind", "SourceURI", "SourceRevision", "HostRequirementsDigest",
			"RuntimeObjectSpecDeclarations", "RuntimeRelatedListRules", "RuntimeSearchScopeOrder", "RuntimeSearchAllDefaultFields", "RuntimeFormulaFunctionBundles", "RuntimeStartupShallowDeclarations",
		}).AddRow(
			fixture.pack.Name(), string(fixture.pack.Category()), installedPackStatusActive,
			metadata.PackVersion, metadata.ManifestFormatVersion, metadata.PlatformCompatibility,
			fixture.payloadHash, fixture.payloadHash, metadata.PayloadHashAlgorithm,
			metadata.SourceKind, metadata.SourceURI, nullStringArg(fixture.row.SourceRevision), nil,
			fixture.declaration.ObjectPayloadsJSON(), fixture.declaration.RelatedListRulesJSON(),
			fixture.declaration.SearchScopeOrderJSON(), fixture.declaration.SearchAllDefaultFieldsJSON(),
			fixture.declaration.FormulaFunctionBundlesJSON(), fixture.declaration.StartupShallowDeclaredRowsJSON(),
		))
}

func TestRestoreInstalledRuntimeMetadataAfterInstallFailureUsesRunnerRegistrationSet(t *testing.T) {
	fixture := newRegisteredRuntimeConsumerFixture(t)
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	fixture.expectActiveRuntimeRow(mock)
	t.Cleanup(func() {
		if err := packs.RegisterRuntimeMetadataForPacks(context.Background(), nil, nil); err != nil {
			t.Errorf("reset runtime metadata: %v", err)
		}
	})

	runner := &Runner{SvcMgr: &runnerServiceGraph{
		ServiceGraph: newTestServiceGraph(testServiceGraphConfig{}),
		options:      fixture.options,
	}}
	if err := restoreInstalledRuntimeMetadataAfterInstallFailure(context.Background(), runner, db); err != nil {
		t.Fatalf("restore registered installed runtime metadata: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestInstalledAppPackForRowUsesRegistrationSet(t *testing.T) {
	fixture := newRegisteredRuntimeConsumerFixture(t)

	pack, err := installedAppPackForRowWithComponentKindRegistrationSet(
		context.Background(), nil, fixture.row, nil, false, fixture.options.ComponentKindRegistrationSet,
	)
	if err != nil || pack == nil || pack.Name() != fixture.row.Name {
		t.Fatalf("registered installed row resolution = (%v, %v)", pack, err)
	}
	if _, err := installedAppPackForRow(context.Background(), nil, fixture.row, nil, false); err == nil || !strings.Contains(err.Error(), "runner_managed_test") {
		t.Fatalf("missing registration error = %v", err)
	}
}

type registeredRuntimeConsumerFixture struct {
	options     RunnerOptions
	pack        packs.Pack
	sourceDir   string
	payloadHash string
	declaration persistedRuntimeDeclarationJSONView
	row         installedPackRow
}

func newRegisteredRuntimeConsumerFixture(t *testing.T) registeredRuntimeConsumerFixture {
	t.Helper()
	rawSource, options := runnerManagedVerifyFixture(t)
	report := packs.ValidateInstallablePackSourceWithComponentKindRegistrationSet(
		rawSource, packs.DefaultPackSourceOptions(), options.ComponentKindRegistrationSet,
	)
	if report.HasErrors() || report.Pack == nil {
		t.Fatalf("validate registered source: %s", report.Error())
	}
	payloadHash, err := packs.PackPayloadHash(report.Pack)
	if err != nil {
		t.Fatal(err)
	}
	declaration, err := packs.BuildPersistedRuntimeDeclaration(report.Pack)
	if err != nil {
		t.Fatal(err)
	}
	metadata := packs.PackHashMetadataForPack(report.Pack)
	return registeredRuntimeConsumerFixture{
		options: options, pack: report.Pack, sourceDir: strings.TrimPrefix(rawSource, "local_path:"),
		payloadHash: payloadHash, declaration: declaration,
		row: installedPackRow{
			Name:                  report.Pack.Name(),
			Category:              string(report.Pack.Category()),
			ManifestFormatVersion: sql.NullString{String: metadata.ManifestFormatVersion, Valid: true},
			AppliedPayloadHash:    sql.NullString{String: payloadHash, Valid: true},
			SourceKind:            sql.NullString{String: metadata.SourceKind, Valid: true},
			SourceURI:             sql.NullString{String: metadata.SourceURI, Valid: true},
			SourceRevision:        sql.NullString{String: metadata.SourceRevision, Valid: metadata.SourceRevisionValid},
		},
	}
}

func (f registeredRuntimeConsumerFixture) expectActiveRuntimeRow(mock sqlmock.Sqlmock) {
	expectActiveInstalledPackRuntimeDeclarationWithFormulaBundles(mock, f.pack, f.sourceDir, f.payloadHash, f.declaration)
}

func isRunnerComponentRegistrationSetCall(expr ast.Expr) bool {
	call, ok := expr.(*ast.CallExpr)
	if !ok || len(call.Args) != 0 {
		return false
	}
	selector, ok := call.Fun.(*ast.SelectorExpr)
	if !ok || selector.Sel == nil || selector.Sel.Name != "componentKindRegistrationSet" {
		return false
	}
	receiver, ok := selector.X.(*ast.Ident)
	return ok && (receiver.Name == "r" || receiver.Name == "runner")
}

func isSelector(expr ast.Expr, receiverName, selectorName string) bool {
	selector, ok := expr.(*ast.SelectorExpr)
	if !ok || selector.Sel == nil || selector.Sel.Name != selectorName {
		return false
	}
	return isIdentifier(selector.X, receiverName)
}

func isIdentifier(expr ast.Expr, name string) bool {
	identifier, ok := expr.(*ast.Ident)
	return ok && identifier.Name == name
}
