package controlplane

import (
	"context"
	"database/sql"
	"errors"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
	"time"

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

var errRunnerManagedVerify = errors.New("runner managed verify reached")

func TestValidatePayloadHashForInstalledPackSourceRevisionOwnsRegisteredSourceResolution(t *testing.T) {
	rawSource, options := runnerManagedVerifyFixture(t)
	report := packs.ValidateInstallablePackSourceWithComponentKindRegistrationSet(
		rawSource, packs.DefaultPackSourceOptions(), options.ComponentKindRegistrationSet,
	)
	if report.HasErrors() || report.Pack == nil {
		t.Fatalf("validate source: %s", report.Error())
	}
	payloadHash, err := packs.PackPayloadHash(report.Pack)
	if err != nil {
		t.Fatal(err)
	}
	metadata := packs.PackHashMetadataForPack(report.Pack)
	row := installedPackRow{
		Name:                  report.Pack.Name(),
		Category:              string(report.Pack.Category()),
		ManifestFormatVersion: sql.NullString{String: metadata.ManifestFormatVersion, 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},
	}

	if err := validatePayloadHashForInstalledPackSourceRevisionWithComponentKindRegistrationSet(
		context.Background(), nil, row, metadata.SourceRevision, payloadHash, options.ComponentKindRegistrationSet,
	); err != nil {
		t.Fatalf("registered source revision validation: %v", err)
	}
	if err := validatePayloadHashForInstalledPackSourceRevision(
		context.Background(), nil, row, metadata.SourceRevision, payloadHash,
	); err == nil || !strings.Contains(err.Error(), "runner_managed_test") {
		t.Fatalf("missing registration error = %v", err)
	}
}

func TestVerifyInstalledPackByNameWithRunnerOwnsRegisteredSourceResolution(t *testing.T) {
	rawSource, options := runnerManagedVerifyFixture(t)
	report := packs.ValidateInstallablePackSourceWithComponentKindRegistrationSet(
		rawSource, packs.DefaultPackSourceOptions(), options.ComponentKindRegistrationSet,
	)
	if report.HasErrors() || report.Pack == nil {
		t.Fatalf("validate source: %s", report.Error())
	}
	payloadHash, err := packs.PackPayloadHash(report.Pack)
	if err != nil {
		t.Fatal(err)
	}
	metadata := packs.PackHashMetadataForPack(report.Pack)

	t.Run("registered source reaches managed verification", func(t *testing.T) {
		db, mock, err := sqlmock.New()
		if err != nil {
			t.Fatal(err)
		}
		t.Cleanup(func() { _ = db.Close() })
		expectRunnerManagedInstalledPack(t, mock, strings.TrimPrefix(rawSource, "local_path:"), payloadHash, metadata)
		mock.ExpectBegin()
		mock.ExpectRollback()

		runner := &Runner{
			DB: database.NewConnectionFromDB(db),
			SvcMgr: &runnerServiceGraph{
				ServiceGraph: newTestServiceGraph(testServiceGraphConfig{}),
				options:      options,
			},
		}
		err = VerifyInstalledPackByNameWithRunner(context.Background(), runner, "runner-managed-test", false)
		if !errors.Is(err, errRunnerManagedVerify) {
			t.Fatalf("registered verify error = %v, want managed provider cause", err)
		}
		if strings.Contains(err.Error(), "resolution") || strings.Contains(err.Error(), "lifecycle is not wired") {
			t.Fatalf("registered verify failed before managed provider: %v", err)
		}
		if err := mock.ExpectationsWereMet(); err != nil {
			t.Fatal(err)
		}
	})

	t.Run("runner options reach managed verification", func(t *testing.T) {
		db, mock, err := sqlmock.New()
		if err != nil {
			t.Fatal(err)
		}
		t.Cleanup(func() { _ = db.Close() })
		expectRunnerManagedInstalledPack(t, mock, strings.TrimPrefix(rawSource, "local_path:"), payloadHash, metadata)
		mock.ExpectBegin()
		mock.ExpectRollback()

		err = VerifyInstalledPackByNameWithRunnerOptions(context.Background(), db, "runner-managed-test", false, options)
		if !errors.Is(err, errRunnerManagedVerify) {
			t.Fatalf("registered verify error = %v, want managed provider cause", err)
		}
		if strings.Contains(err.Error(), "resolution") || strings.Contains(err.Error(), "lifecycle is not wired") {
			t.Fatalf("registered verify failed before managed provider: %v", err)
		}
		if err := mock.ExpectationsWereMet(); err != nil {
			t.Fatal(err)
		}
	})

	t.Run("missing registration fails before lifecycle", func(t *testing.T) {
		db, mock, err := sqlmock.New()
		if err != nil {
			t.Fatal(err)
		}
		t.Cleanup(func() { _ = db.Close() })
		expectRunnerManagedInstalledPack(t, mock, strings.TrimPrefix(rawSource, "local_path:"), payloadHash, metadata)

		runner := &Runner{DB: database.NewConnectionFromDB(db)}
		err = VerifyInstalledPackByNameWithRunner(context.Background(), runner, "runner-managed-test", false)
		if err == nil || !strings.Contains(err.Error(), "runner_managed_test") || !strings.Contains(err.Error(), "resolution") {
			t.Fatalf("missing registration error = %v", err)
		}
		if strings.Contains(err.Error(), "lifecycle is not wired") {
			t.Fatalf("missing registration reached lifecycle: %v", err)
		}
		if err := mock.ExpectationsWereMet(); err != nil {
			t.Fatal(err)
		}
	})
}

func runnerManagedVerifyFixture(t *testing.T) (string, RunnerOptions) {
	t.Helper()
	root := t.TempDir()
	manifest := `{"manifestFormatVersion":"2","name":"runner-managed-test","displayName":"Runner Managed Test","category":"app-pack","version":"0.1.0","platformCompatibility":{"min":"0.2.0","maxExclusive":"0.3.0"},"dependencies":[],"components":[{"kind":"runner_managed_test","businessKey":"Test/One","manageability":"locked","path":"components/one.json"}]}`
	if err := os.MkdirAll(filepath.Join(root, "components"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "pack.json"), []byte(manifest), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "components", "one.json"), []byte(`{}`), 0o644); err != nil {
		t.Fatal(err)
	}

	kind := cp.ComponentKind("runner_managed_test")
	manageabilities := map[cp.Manageability]struct{}{cp.ManageabilityLocked: {}}
	registration := cp.ComponentKindRegistration{
		Contract: cp.ComponentKindContract{
			Kind: kind, Phase: cp.InstallPlanPhaseAutomation,
			WriteSet: []cp.ComponentWriteSurface{{Table: "RunnerManagedTest", Fact: "test rows"}}, BusinessKey: "test key",
			OwnerDiscriminator: cp.ComponentOwnerStrategyOwnerPack, Manageabilities: manageabilities,
			VerifyProjection: "test rows", CollisionKey: "test key",
		},
		Capability:        cp.SourceBackedComponentCapability{Kind: kind, Installable: true, Manageabilities: manageabilities},
		AllowedCategories: []cp.PackCategory{cp.AppPack},
		BuildHandler: func(ctx cp.ComponentBuildContext, _ cp.ComponentBuildRequest) error {
			return ctx.AddManagedComponent(cp.ComponentBuildManagedComponent{ExpectedProjection: []byte(`{"rows":[]}`)})
		},
	}
	lifecycle, err := cp.NewComponentManagedLifecycleRegistration(cp.ComponentManagedLifecycleRegistrationSpec{
		Kind: kind, ReadSet: []cp.ComponentReadSurface{{Table: "RunnerManagedTest", Fact: "test rows"}},
		Capabilities: []cp.ComponentManagedCapability{
			cp.ComponentManagedCapabilityTargetSerialization, cp.ComponentManagedCapabilityStructuredMutation,
			cp.ComponentManagedCapabilityEvidence, cp.ComponentManagedCapabilityFreshProjection,
		},
		TransactionMode: cp.ComponentManagedTransactionPessimisticReadCommitted,
		Budget: cp.ComponentManagedBudget{
			MaxTargets: 1, MaxCurrentItems: 1, MaxDesiredItems: 1, MaxDeltaRows: 1,
			MaxMutationBytes: 1024, MaxMutationStatements: 1, MaxBatchRows: 1,
			MaxEvidenceWrites: 1, MaxEffectCalls: 1,
			MaxLockWait: time.Second, MaxElapsed: time.Second, MaxAttempts: 1,
		},
		Lifecycle: cp.ComponentManagedLifecycleProvider{
			Plan: func(context.Context, cp.ComponentManagedScope, cp.ComponentManagedLifecycleRequest, cp.ComponentManagedReadStore) (cp.ComponentManagedPlan, error) {
				return cp.ComponentManagedPlan{}, nil
			},
			Mutate: func(context.Context, cp.ComponentManagedScope, cp.ComponentManagedLifecycleRequest, cp.ComponentManagedPlan, cp.ComponentManagedMutationStore) (cp.ComponentManagedMutationResult, error) {
				return cp.ComponentManagedMutationResult{Disposition: cp.ComponentManagedMutationNoChange}, nil
			},
			Project: func(context.Context, cp.ComponentManagedScope, cp.ComponentManagedLifecycleRequest, cp.ComponentManagedReadStore) (cp.ComponentManagedProjection, error) {
				return cp.ComponentManagedProjection{}, errRunnerManagedVerify
			},
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	bound, err := registration.WithManagedLifecycle(lifecycle)
	if err != nil {
		t.Fatal(err)
	}
	set, err := packs.NewComponentKindRegistrationSetWithManagedLifecycles(nil, []cp.ComponentKindManagedLifecycleRegistration{bound})
	if err != nil {
		t.Fatal(err)
	}
	return "local_path:" + root, RunnerOptions{ComponentKindRegistrationSet: set}
}

func expectRunnerManagedInstalledPack(t *testing.T, mock sqlmock.Sqlmock, sourcePath, payloadHash string, metadata packs.PackHashMetadata) {
	t.Helper()
	mock.ExpectQuery(regexp.QuoteMeta("SELECT Name, Category, Status, PackVersion, ManifestFormatVersion, PlatformCompatibility, DesiredPayloadHash, AppliedPayloadHash, PayloadHashAlgorithm, SourceKind, SourceURI, SourceRevision, HostRequirementsDigest FROM _System_InstalledPack WHERE COALESCE(IsDeleted, 0) = 0 AND Name = ? LIMIT 1")).
		WithArgs("runner-managed-test").
		WillReturnRows(sqlmock.NewRows([]string{
			"Name", "Category", "Status", "PackVersion", "ManifestFormatVersion", "PlatformCompatibility", "DesiredPayloadHash", "AppliedPayloadHash", "PayloadHashAlgorithm", "SourceKind", "SourceURI", "SourceRevision", "HostRequirementsDigest",
		}).AddRow(
			"runner-managed-test", string(packs.AppPack), installedPackStatusActive,
			metadata.PackVersion, metadata.ManifestFormatVersion, metadata.PlatformCompatibility,
			payloadHash, payloadHash, metadata.PayloadHashAlgorithm,
			string(packs.PackSourceKindLocalPath), sourcePath, nil, nil,
		))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT Status, COALESCE(IsDeleted, 0) FROM _System_InstalledPack WHERE Name = ? LIMIT 1")).
		WithArgs("runner-managed-test").
		WillReturnRows(sqlmock.NewRows([]string{"Status", "IsDeleted"}).AddRow(installedPackStatusActive, 0))
	expectNoPackOwnedApprovalProcesses(mock, "runner-managed-test")
}
