package controlplane

import (
	"errors"
	"reflect"
	"strings"
	"testing"

	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

func TestResolveApplyAllSourceRevisionAdvanceSourceTupleEmpty(t *testing.T) {
	tuple, err := resolveApplyAllSourceRevisionAdvanceSourceTuple(nil, packs.ComponentKindRegistrationSet{})
	if err != nil {
		t.Fatalf("resolve empty source tuple: %v", err)
	}
	if len(tuple.plan) != 0 || len(tuple.targets) != 0 {
		t.Fatalf("empty tuple = %#v, want initialized empty plan and targets", tuple)
	}
	resolution, err := resolveApplyAllSourceRevisionAdvanceSourcesAtRevisions(nil, nil, packs.ComponentKindRegistrationSet{})
	if err != nil || resolution.targets == nil || len(resolution.targets) != 0 {
		t.Fatalf("empty source resolution = %#v, %v; want initialized empty targets", resolution, err)
	}
}

func TestApplyAllSourceRevisionAdvancePackSourceOptionsAtRevisions(t *testing.T) {
	provider := testApplyAllSourceRevisionAdvance("explicit-provider", 1)
	consumer := testApplyAllSourceRevisionAdvance("explicit-consumer", 0)
	plan := []normalizedApplyAllSourceRevisionAdvance{consumer, provider}
	revisions := map[string]string{provider.URI: provider.FromRevision, consumer.URI: consumer.FromRevision}
	originalRevisions := map[string]string{provider.URI: provider.FromRevision, consumer.URI: consumer.FromRevision}

	options, err := applyAllSourceRevisionAdvancePackSourceOptionsAtRevisions(plan, revisions)
	if err != nil {
		t.Fatalf("compile explicit source revisions: %v", err)
	}
	for name, revision := range revisions {
		if got := options.OfficialRepoSourceRevisions[name]; got != revision {
			t.Fatalf("revision map[%q] = %q, want %q", name, got, revision)
		}
	}
	if !reflect.DeepEqual(revisions, originalRevisions) {
		t.Fatalf("explicit revisions were mutated: got %#v, want %#v", revisions, originalRevisions)
	}
}

func TestApplyAllSourceRevisionAdvancePackSourceOptionsAtRevisionsRejectsInvalidMap(t *testing.T) {
	advance := testApplyAllSourceRevisionAdvance("explicit-pack", 4)
	tests := []struct {
		name      string
		revisions map[string]string
		index     int
		packName  string
	}{
		{name: "missing", revisions: map[string]string{}, index: -1},
		{name: "extra", revisions: map[string]string{"explicit-pack": advance.FromRevision, "extra": "extra-revision"}, index: -1},
		{name: "noncanonical key", revisions: map[string]string{"Explicit-Pack": advance.FromRevision}, index: 4, packName: advance.Name},
		{name: "case collision", revisions: map[string]string{"explicit-pack": advance.FromRevision, "Explicit-Pack": advance.FromRevision}, index: -1},
		{name: "blank revision", revisions: map[string]string{"explicit-pack": "  "}, index: 4, packName: advance.Name},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := applyAllSourceRevisionAdvancePackSourceOptionsAtRevisions(
				[]normalizedApplyAllSourceRevisionAdvance{advance}, test.revisions,
			)
			assertApplyAllSourceRevisionComposeError(t, err, test.index, test.packName)
		})
	}
}

func TestResolveApplyAllSourceRevisionAdvanceSourcesAtRevisionsReturnsExactSnapshot(t *testing.T) {
	provider := testApplyAllSourceRevisionAdvance("explicit-provider", 1)
	consumer := testApplyAllSourceRevisionAdvance("explicit-consumer", 0)
	plan := []normalizedApplyAllSourceRevisionAdvance{consumer, provider}
	revisions := map[string]string{provider.URI: provider.FromRevision, consumer.URI: consumer.FromRevision}
	sources := map[string]*packs.ResolvedPackSource{
		provider.Name: testApplyAllSourceRevisionSource(provider.Name, "2.1.0", provider.FromRevision),
		consumer.Name: testApplyAllSourceRevisionSource(consumer.Name, "1.0.0", consumer.FromRevision),
	}
	registrations := runnerTestRegistrationSet(t)

	originalValidator := validateApplyAllSourceRevisionSource
	t.Cleanup(func() { validateApplyAllSourceRevisionSource = originalValidator })
	calls := 0
	validateApplyAllSourceRevisionSource = func(
		raw string,
		opts packs.PackSourceOptions,
		gotRegistrations packs.ComponentKindRegistrationSet,
	) packs.PackValidationReport {
		calls++
		if opts.ObjectSchemaResolver != nil {
			t.Fatal("explicit requested-source resolution received a live object-schema fallback")
		}
		for name, revision := range revisions {
			if got := opts.OfficialRepoSourceRevisions[name]; got != revision {
				t.Fatalf("revision map[%q] = %q, want %q before lookup %d", name, got, revision, calls)
			}
		}
		assertApplyAllSourceRevisionRunnerTestRegistration(t, gotRegistrations)
		return packs.PackValidationReport{Source: sources[strings.TrimPrefix(raw, "official_repo:")]}
	}

	resolution, err := resolveApplyAllSourceRevisionAdvanceSourcesAtRevisions(plan, revisions, registrations)
	if err != nil {
		t.Fatalf("resolve sources at explicit revisions: %v", err)
	}
	if calls != 2 || len(resolution.targets) != 2 {
		t.Fatalf("source calls/targets = %d/%d, want 2/2", calls, len(resolution.targets))
	}
	for name, revision := range revisions {
		meta := packs.PackHashMetadataForPack(resolution.targets[name])
		if !meta.SourceRevisionValid || meta.SourceRevision != revision || resolution.sourceOptions.OfficialRepoSourceRevisions[name] != revision {
			t.Fatalf("resolved %s revision/options = %q/%q, want %q", name, meta.SourceRevision, resolution.sourceOptions.OfficialRepoSourceRevisions[name], revision)
		}
	}
	revisions[provider.URI] = "caller-mutated"
	if resolution.sourceOptions.OfficialRepoSourceRevisions[provider.URI] != provider.FromRevision {
		t.Fatal("resolved source options alias the caller revision map")
	}
	resolution.sourceOptions.OfficialRepoSourceRevisions[consumer.URI] = "result-mutated"
	if revisions[consumer.URI] != consumer.FromRevision {
		t.Fatal("caller revision map aliases the resolved source options")
	}
}

func TestResolveApplyAllSourceRevisionAdvanceReleaseRepoUsesOperationAuthority(t *testing.T) {
	advance := testApplyAllSourceRevisionAdvance("release-pack", 3)
	advance.Kind = string(packs.PackSourceKindReleaseRepo)
	advance.URI = "urn:nexuscrm:pack:release-pack"
	advance.FromRevision = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	advance.ToRevision = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
	plan := []normalizedApplyAllSourceRevisionAdvance{advance}
	base := packs.DefaultPackSourceOptions()
	base.ReleaseRepoSources = map[string]packs.ReleaseRepoSource{
		advance.URI: {Root: "/exact/repository/packs/release-pack", Revision: advance.ToRevision},
	}

	originalValidator := validateApplyAllSourceRevisionSource
	t.Cleanup(func() { validateApplyAllSourceRevisionSource = originalValidator })
	validateApplyAllSourceRevisionSource = func(
		raw string,
		opts packs.PackSourceOptions,
		_ packs.ComponentKindRegistrationSet,
	) packs.PackValidationReport {
		if raw != string(packs.PackSourceKindReleaseRepo)+":"+advance.URI {
			t.Fatalf("resolved source = %q, want release_repo identity", raw)
		}
		authority, ok := opts.ReleaseRepoSources[advance.URI]
		if !ok || authority.Root != "/exact/repository/packs/release-pack" || authority.Revision != advance.ToRevision {
			t.Fatalf("release authority = %#v, %v; want exact operation root and target revision", authority, ok)
		}
		source := testApplyAllSourceRevisionSource(advance.Name, "1.0.0", advance.ToRevision)
		source.Kind = advance.Kind
		source.URI = advance.URI
		return packs.PackValidationReport{Source: source}
	}

	resolution, err := resolveApplyAllSourceRevisionAdvanceSourcesAtRevisionsWithBase(
		plan,
		map[string]string{strings.ToLower(advance.URI): advance.ToRevision},
		packs.ComponentKindRegistrationSet{},
		base,
	)
	if err != nil {
		t.Fatalf("resolve release source revision: %v", err)
	}
	if got := applyAllSourceRevisionFromOptions(advance, resolution.sourceOptions); got != advance.ToRevision {
		t.Fatalf("resolved release revision = %q, want %q", got, advance.ToRevision)
	}
	if base.ReleaseRepoSources[advance.URI].Revision != advance.ToRevision {
		t.Fatal("base release authority was mutated")
	}
}

func TestResolveApplyAllSourceRevisionAdvanceReleaseRepoRejectsMissingOperationAuthority(t *testing.T) {
	advance := testApplyAllSourceRevisionAdvance("release-pack", 3)
	advance.Kind = string(packs.PackSourceKindReleaseRepo)
	advance.URI = "urn:nexuscrm:pack:release-pack"
	advance.ToRevision = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"

	_, err := resolveApplyAllSourceRevisionAdvanceSourcesAtRevisionsWithBase(
		[]normalizedApplyAllSourceRevisionAdvance{advance},
		map[string]string{strings.ToLower(advance.URI): advance.ToRevision},
		packs.ComponentKindRegistrationSet{},
		packs.DefaultPackSourceOptions(),
	)
	composeErr := assertApplyAllSourceRevisionComposeError(t, err, advance.index, advance.Name)
	if !strings.Contains(composeErr.Error(), "no exact operation authority") {
		t.Fatalf("missing authority error = %v", composeErr)
	}
}

func TestResolveApplyAllSourceRevisionAdvanceSourceTuplePinsCompleteRevisionSetBeforeLookup(t *testing.T) {
	const (
		providerName   = "tuple-provider"
		consumerName   = "tuple-consumer"
		providerTarget = "provider-target"
		consumerTarget = "consumer-target"
	)
	provider := testApplyAllSourceRevisionAdvance(providerName, 1)
	provider.ToRevision = providerTarget
	consumer := testApplyAllSourceRevisionAdvance(consumerName, 0)
	consumer.ToRevision = consumerTarget
	plan := []normalizedApplyAllSourceRevisionAdvance{consumer, provider}
	originalPlan := append([]normalizedApplyAllSourceRevisionAdvance(nil), plan...)
	sources := map[string]*packs.ResolvedPackSource{
		providerName: testApplyAllSourceRevisionSource(providerName, "2.1.0", providerTarget),
		consumerName: testApplyAllSourceRevisionSource(
			consumerName, "1.0.0", consumerTarget,
			packs.PackDependency{Name: providerName, VersionConstraint: ">=2.0.0"},
		),
	}
	registrations := runnerTestRegistrationSet(t)

	originalValidator := validateApplyAllSourceRevisionSource
	t.Cleanup(func() { validateApplyAllSourceRevisionSource = originalValidator })
	calls := 0
	validateApplyAllSourceRevisionSource = func(
		raw string,
		opts packs.PackSourceOptions,
		gotRegistrations packs.ComponentKindRegistrationSet,
	) packs.PackValidationReport {
		calls++
		if opts.ObjectSchemaResolver != nil {
			t.Fatal("requested-source validation unexpectedly received a live object-schema fallback")
		}
		for name, revision := range map[string]string{providerName: providerTarget, consumerName: consumerTarget} {
			if got := opts.OfficialRepoSourceRevisions[name]; got != revision {
				t.Fatalf("revision map[%q] = %q, want %q before lookup %d", name, got, revision, calls)
			}
		}
		got := gotRegistrations.Registrations()
		if len(got) != 1 || string(got[0].Contract.Kind) != "runner_test" {
			t.Fatalf("source validation registrations = %#v, want runner_test", got)
		}
		name := strings.TrimPrefix(raw, "official_repo:")
		if raw != "official_repo:"+name || sources[name] == nil {
			t.Fatalf("validated source = %q, want a planned official repository", raw)
		}
		return packs.PackValidationReport{Source: sources[name]}
	}

	tuple, err := resolveApplyAllSourceRevisionAdvanceSourceTuple(plan, registrations)
	if err != nil {
		t.Fatalf("resolve source tuple: %v", err)
	}
	if calls != 2 {
		t.Fatalf("source validation calls = %d, want 2", calls)
	}
	if got := []string{tuple.plan[0].Name, tuple.plan[1].Name}; !reflect.DeepEqual(got, []string{providerName, consumerName}) {
		t.Fatalf("ordered tuple = %v, want provider before consumer", got)
	}
	for name, revision := range map[string]string{providerName: providerTarget, consumerName: consumerTarget} {
		meta := packs.PackHashMetadataForPack(tuple.targets[name])
		if !meta.SourceRevisionValid || meta.SourceRevision != revision {
			t.Fatalf("target %s revision = %q, want %q", name, meta.SourceRevision, revision)
		}
	}
	if !reflect.DeepEqual(plan, originalPlan) {
		t.Fatalf("input plan was mutated: got %#v, want %#v", plan, originalPlan)
	}
}

func TestResolveApplyAllSourceRevisionAdvanceSourceTuplePreservesOriginalErrorIdentity(t *testing.T) {
	const packName = "wrong-target-revision"
	advance := testApplyAllSourceRevisionAdvance(packName, 5)

	originalValidator := validateApplyAllSourceRevisionSource
	t.Cleanup(func() { validateApplyAllSourceRevisionSource = originalValidator })
	validateApplyAllSourceRevisionSource = func(
		string,
		packs.PackSourceOptions,
		packs.ComponentKindRegistrationSet,
	) packs.PackValidationReport {
		return packs.PackValidationReport{Source: testApplyAllSourceRevisionSource(packName, "1.0.0", "wrong")}
	}

	_, err := resolveApplyAllSourceRevisionAdvanceSourceTuple(
		[]normalizedApplyAllSourceRevisionAdvance{advance}, packs.ComponentKindRegistrationSet{},
	)
	composeErr := assertApplyAllSourceRevisionComposeError(t, err, 5, packName)
	if !strings.Contains(composeErr.Error(), "identity/revision") {
		t.Fatalf("compose error = %v, want exact target revision failure", composeErr)
	}
}

func testApplyAllSourceRevisionSource(
	name string,
	version string,
	revision string,
	dependencies ...packs.PackDependency,
) *packs.ResolvedPackSource {
	return &packs.ResolvedPackSource{
		Kind: string(packs.PackSourceKindOfficialRepo), URI: name, Revision: revision,
		Manifest: &packs.PackManifest{
			ManifestFormatVersion: packs.SupportedManifestFormatVersion,
			Name:                  name, DisplayName: name, Category: packs.AppPack, Version: version,
			PlatformCompatibility: packs.PlatformCompatibility{Min: "0.2.0", MaxExclusive: "0.3.0"},
			Dependencies:          dependencies,
			Components:            []packs.ManifestComponent{},
		},
		Files: map[string][]byte{}, FileRefs: []string{},
	}
}

func assertApplyAllSourceRevisionComposeError(t *testing.T, err error, index int, name string) *ApplyAllSourceRevisionAdvancePlanError {
	t.Helper()
	var planErr *ApplyAllSourceRevisionAdvancePlanError
	if !errors.As(err, &planErr) {
		t.Fatalf("error = %v, want ApplyAllSourceRevisionAdvancePlanError", err)
	}
	if planErr.Stage != ApplyAllSourceRevisionAdvancePlanCompose || planErr.Index != index || planErr.Name != name {
		t.Fatalf("plan error = %#v, want stage=%q index=%d name=%q", planErr, ApplyAllSourceRevisionAdvancePlanCompose, index, name)
	}
	return planErr
}
