package controlplane

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"testing"

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

func TestPreflightActiveInstalledPackGenerationAuthoritiesRejectsActiveConnectorWithoutSourceIdentity(t *testing.T) {
	rawDB, _, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer rawDB.Close()

	row := installedPackRow{
		Name:     "legacy-connector",
		Category: string(packs.ConnectorPack),
		Status:   installedPackStatusActive,
	}
	runner := &Runner{DB: database.NewConnectionFromDB(rawDB)}
	err = preflightActiveInstalledPackGenerationAuthorities(
		context.Background(),
		runner,
		[]installedPackRow{row},
		packs.ComponentKindRegistrationSet{},
	)
	if !errors.Is(err, ErrPackGenerationPrecondition) {
		t.Fatalf("preflight error = %v, want generation precondition failure", err)
	}
	if !strings.Contains(err.Error(), row.Name) {
		t.Fatalf("preflight error = %v, want pack name %q", err, row.Name)
	}
}

func TestClassifySourceBackedPackReconcileLifecycle(t *testing.T) {
	officialKind := sql.NullString{
		String: string(packs.PackSourceKindOfficialRepo),
		Valid:  true,
	}
	localKind := sql.NullString{
		String: string(packs.PackSourceKindLocalPath),
		Valid:  true,
	}
	sourceURI := sql.NullString{String: "authority-fixture", Valid: true}
	sourceRevision := sql.NullString{String: "revision-1", Valid: true}
	tests := []struct {
		name          string
		row           installedPackRow
		wantLifecycle sourceBackedPackReconcileLifecycle
		wantErr       bool
	}{
		{
			name: "active official revision bound",
			row: installedPackRow{
				Name: "active-official", Status: installedPackStatusActive,
				SourceKind: officialKind, SourceURI: sourceURI, SourceRevision: sourceRevision,
			},
			wantLifecycle: sourceBackedPackReconcileLifecycleActive,
		},
		{
			name: "active local path uses current source",
			row: installedPackRow{
				Name: "active-local", Status: installedPackStatusActive,
				SourceKind: localKind, SourceURI: sourceURI,
			},
			wantLifecycle: sourceBackedPackReconcileLifecycleActive,
		},
		{
			name: "installing revision bound",
			row: installedPackRow{
				Name: "installing", Status: installedPackStatusInstalling,
				SourceKind: localKind, SourceURI: sourceURI, SourceRevision: sourceRevision,
			},
			wantLifecycle: sourceBackedPackReconcileLifecycleRetry,
		},
		{
			name: "failed revision bound",
			row: installedPackRow{
				Name: "failed", Status: installedPackStatusFailed,
				SourceKind: officialKind, SourceURI: sourceURI, SourceRevision: sourceRevision,
			},
			wantLifecycle: sourceBackedPackReconcileLifecycleRetry,
		},
		{
			name: "retry revision absent",
			row: installedPackRow{
				Name: "unbound-failed", Status: installedPackStatusFailed,
				SourceKind: localKind, SourceURI: sourceURI,
			},
			wantErr: true,
		},
		{
			name: "active identity absent",
			row: installedPackRow{
				Name: "identityless-active", Status: installedPackStatusActive,
			},
			wantErr: true,
		},
		{
			name: "active official revision absent",
			row: installedPackRow{
				Name: "unbound-official", Status: installedPackStatusActive,
				SourceKind: officialKind, SourceURI: sourceURI,
			},
			wantErr: true,
		},
		{
			name: "installing identity absent",
			row: installedPackRow{
				Name: "identityless-installing", Status: installedPackStatusInstalling,
			},
			wantErr: true,
		},
		{
			name: "failed identity partial",
			row: installedPackRow{
				Name: "partial-failed", Status: installedPackStatusFailed,
				SourceKind: officialKind, SourceRevision: sourceRevision,
			},
			wantErr: true,
		},
		{
			name: "source kind unsupported",
			row: installedPackRow{
				Name: "unsupported-source", Status: installedPackStatusActive,
				SourceKind: sql.NullString{String: "repository", Valid: true},
				SourceURI:  sourceURI, SourceRevision: sourceRevision,
			},
			wantErr: true,
		},
		{
			name: "removing unsupported",
			row: installedPackRow{
				Name: "removing", Status: installedPackStatusRemoving,
				SourceKind: officialKind, SourceURI: sourceURI, SourceRevision: sourceRevision,
			},
			wantErr: true,
		},
		{
			name: "non canonical active unsupported",
			row: installedPackRow{
				Name: "noncanonical-active", Status: " active ",
				SourceKind: officialKind, SourceURI: sourceURI, SourceRevision: sourceRevision,
			},
			wantErr: true,
		},
		{
			name: "unknown lifecycle unsupported",
			row: installedPackRow{
				Name: "unknown", Status: "paused",
				SourceKind: officialKind, SourceURI: sourceURI, SourceRevision: sourceRevision,
			},
			wantErr: true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got, err := classifySourceBackedPackReconcileLifecycle(test.row)
			if test.wantErr {
				if !errors.Is(err, ErrPackGenerationPrecondition) {
					t.Fatalf("classification error = %v, want generation precondition failure", err)
				}
				return
			}
			if err != nil {
				t.Fatalf("classification error: %v", err)
			}
			if got != test.wantLifecycle {
				t.Fatalf("lifecycle = %v, want %v", got, test.wantLifecycle)
			}
		})
	}
}

func TestPreflightRetryableInstalledPackSourceStateRejectsUnclassifiedPeer(t *testing.T) {
	rawDB, _, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer rawDB.Close()

	completeKind := sql.NullString{
		String: string(packs.PackSourceKindOfficialRepo),
		Valid:  true,
	}
	completeURI := sql.NullString{String: "authority-fixture", Valid: true}
	completeRevision := sql.NullString{String: "revision-1", Valid: true}
	row := installedPackRow{
		Name:           "noncanonical-active",
		Category:       string(packs.ConnectorPack),
		Status:         " active ",
		SourceKind:     completeKind,
		SourceURI:      completeURI,
		SourceRevision: completeRevision,
	}

	runner := &Runner{DB: database.NewConnectionFromDB(rawDB)}
	admissions, err := preflightRetryableInstalledPackSourceState(
		context.Background(),
		runner,
		[]installedPackRow{row},
		packs.ComponentKindRegistrationSet{},
	)
	if !errors.Is(err, ErrPackGenerationPrecondition) {
		t.Fatalf("preflight error = %v, want generation precondition failure", err)
	}
	if admissions != nil {
		t.Fatalf("admissions = %#v, want nil", admissions)
	}
}

func TestReconcileRejectsUnclassifiedActivePeerBeforeRetryAdmissionOrMutation(t *testing.T) {
	rawDB, _, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer rawDB.Close()

	activeWithoutIdentity := installedPackRow{
		Name:     "legacy-connector",
		Category: string(packs.ConnectorPack),
		Status:   installedPackStatusActive,
	}
	retryRow := installedPackRow{
		Name:          "retry-peer",
		Category:      string(packs.ConnectorPack),
		Status:        installedPackStatusFailed,
		PackInstallID: "retry-installation",
	}

	origList := listInstalledAppPacksForReconcile
	origGenerationPreflight := preflightPackGenerationForReconcile
	origRetryPreflight := preflightRetryableInstalledPackSourceStateForReconcile
	origRebuild := rebuildRepairPackWithAllowedOwnerlessObjectsForReconcile
	origApply := applyResolvedSourceBackedPackUnlockedForReconcile
	origChildCloneBackfill := backfillChildCloneRulePackOwnershipForReconcile
	origExclusiveSiblingBackfill := backfillExclusiveSiblingFieldPackOwnershipForReconcile
	defer func() {
		listInstalledAppPacksForReconcile = origList
		preflightPackGenerationForReconcile = origGenerationPreflight
		preflightRetryableInstalledPackSourceStateForReconcile = origRetryPreflight
		rebuildRepairPackWithAllowedOwnerlessObjectsForReconcile = origRebuild
		applyResolvedSourceBackedPackUnlockedForReconcile = origApply
		backfillChildCloneRulePackOwnershipForReconcile = origChildCloneBackfill
		backfillExclusiveSiblingFieldPackOwnershipForReconcile = origExclusiveSiblingBackfill
	}()

	listInstalledAppPacksForReconcile = func(context.Context, *sql.DB) ([]installedPackRow, error) {
		return []installedPackRow{activeWithoutIdentity, retryRow}, nil
	}
	preflightPackGenerationForReconcile = preflightActiveInstalledPackGenerationAuthorities

	retryPreflightCalls := 0
	rebuildCalls := 0
	applyCalls := 0
	childCloneBackfillCalls := 0
	exclusiveSiblingBackfillCalls := 0
	retryPack := testControlplaneSourceBackedPack(t)
	preflightRetryableInstalledPackSourceStateForReconcile = func(
		context.Context,
		*Runner,
		[]installedPackRow,
		packs.ComponentKindRegistrationSet,
	) ([]retryableInstalledPackSourceStateAdmission, error) {
		retryPreflightCalls++
		return []retryableInstalledPackSourceStateAdmission{{
			row:  retryRow,
			pack: retryPack,
		}}, nil
	}
	rebuildRepairPackWithAllowedOwnerlessObjectsForReconcile = func(
		context.Context,
		*sql.DB,
		installedPackRow,
		*packs.SourceBackedPack,
		InstallPackOptions,
		*installPackPreflightOverrides,
	) (*packs.SourceBackedPack, error) {
		rebuildCalls++
		return retryPack, nil
	}
	applyResolvedSourceBackedPackUnlockedForReconcile = func(
		context.Context,
		*Runner,
		*packs.SourceBackedPack,
		*installPackPreflightOverrides,
		InstallPackOptions,
	) error {
		applyCalls++
		return nil
	}
	backfillChildCloneRulePackOwnershipForReconcile = func(
		context.Context,
		*sql.DB,
		packs.ComponentKindRegistrationSet,
	) (int, error) {
		childCloneBackfillCalls++
		return 0, nil
	}
	backfillExclusiveSiblingFieldPackOwnershipForReconcile = func(
		context.Context,
		*sql.DB,
		packs.ComponentKindRegistrationSet,
	) (int, error) {
		exclusiveSiblingBackfillCalls++
		return 0, nil
	}

	err = reconcileActiveInstalledPackSourceState(
		context.Background(),
		&Runner{DB: database.NewConnectionFromDB(rawDB)},
		InstallPackOptions{allowInstalledPackSourceStateRepair: true},
	)
	if !errors.Is(err, ErrPackGenerationPrecondition) {
		t.Fatalf("reconcile error = %v, want generation precondition failure", err)
	}
	if retryPreflightCalls != 0 ||
		rebuildCalls != 0 ||
		applyCalls != 0 ||
		childCloneBackfillCalls != 0 ||
		exclusiveSiblingBackfillCalls != 0 {
		t.Fatalf(
			"calls after invalid active authority = retry-preflight:%d rebuild:%d apply:%d child-clone-backfill:%d exclusive-sibling-backfill:%d, want all zero",
			retryPreflightCalls,
			rebuildCalls,
			applyCalls,
			childCloneBackfillCalls,
			exclusiveSiblingBackfillCalls,
		)
	}
}
