package controlplane

import (
	"reflect"
	"strings"
	"testing"

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

func TestValidateApplyAllSourceRevisionAdvanceTargetRequiresExactCanonicalTuple(t *testing.T) {
	advance := testApplyAllSourceRevisionAdvance("crm", 7)
	tests := []struct {
		name        string
		target      *packs.SourceBackedPack
		wantErr     bool
		wantErrText string
	}{
		{name: "exact", target: testApplyAllSourceRevisionTarget(t, "crm", "1.0.0", "official_repo", "crm", "target")},
		{name: "supported legacy alias", target: testApplyAllSourceRevisionTarget(t, "crm", "1.0.0", "in_repo", "crm", "target")},
		{name: "nil", target: nil, wantErr: true, wantErrText: "name does not match"},
		{name: "wrong name", target: testApplyAllSourceRevisionTarget(t, "other", "1.0.0", "official_repo", "crm", "target"), wantErr: true, wantErrText: "name does not match"},
		{name: "wrong kind", target: testApplyAllSourceRevisionTarget(t, "crm", "1.0.0", "local_path", "crm", "target"), wantErr: true, wantErrText: "identity/revision"},
		{name: "wrong URI", target: testApplyAllSourceRevisionTarget(t, "crm", "1.0.0", "official_repo", "other", "target"), wantErr: true, wantErrText: "identity/revision"},
		{name: "missing revision", target: testApplyAllSourceRevisionTarget(t, "crm", "1.0.0", "official_repo", "crm", ""), wantErr: true, wantErrText: "identity/revision"},
		{name: "wrong revision", target: testApplyAllSourceRevisionTarget(t, "crm", "1.0.0", "official_repo", "crm", "other"), wantErr: true, wantErrText: "identity/revision"},
		{name: "delimiter smuggled kind", target: testApplyAllSourceRevisionTarget(t, "crm", "1.0.0", "official_repo:other", "crm", "target"), wantErr: true, wantErrText: "identity/revision"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := validateApplyAllSourceRevisionAdvanceTarget(advance, test.target)
			if test.wantErr {
				if err == nil || !strings.Contains(err.Error(), test.wantErrText) {
					t.Fatalf("validate target error = %v, want containing %q", err, test.wantErrText)
				}
				return
			}
			if err != nil {
				t.Fatalf("validate target: %v", err)
			}
		})
	}
}

func TestOrderApplyAllSourceRevisionAdvanceTupleDependenciesFirstAndStable(t *testing.T) {
	consumer := testApplyAllSourceRevisionAdvance("consumer", 0)
	unrelated := testApplyAllSourceRevisionAdvance("unrelated", 1)
	provider := testApplyAllSourceRevisionAdvance("provider", 2)
	plan := []normalizedApplyAllSourceRevisionAdvance{consumer, unrelated, provider}
	targets := map[string]*packs.SourceBackedPack{
		"consumer": testApplyAllSourceRevisionTarget(t, "consumer", "1.0.0", "official_repo", "consumer", "target",
			packs.PackDependency{Name: "provider", VersionConstraint: ">=2.0.0"},
			packs.PackDependency{Name: "external", VersionConstraint: ">=9.0.0"},
		),
		"unrelated": testApplyAllSourceRevisionTarget(t, "unrelated", "1.0.0", "official_repo", "unrelated", "target"),
		"provider":  testApplyAllSourceRevisionTarget(t, "provider", "2.1.0", "official_repo", "provider", "target"),
	}

	ordered, err := orderApplyAllSourceRevisionAdvanceTuple(plan, targets)
	if err != nil {
		t.Fatalf("order tuple: %v", err)
	}
	want := []normalizedApplyAllSourceRevisionAdvance{unrelated, provider, consumer}
	if !reflect.DeepEqual(ordered, want) {
		t.Fatalf("ordered tuple = %#v, want %#v", ordered, want)
	}
	if !reflect.DeepEqual(plan, []normalizedApplyAllSourceRevisionAdvance{consumer, unrelated, provider}) {
		t.Fatalf("input plan was mutated: %#v", plan)
	}
}

func TestOrderApplyAllSourceRevisionAdvanceTupleRejectsIncompleteTargetMap(t *testing.T) {
	advance := testApplyAllSourceRevisionAdvance("crm", 0)
	exact := testApplyAllSourceRevisionTarget(t, "crm", "1.0.0", "official_repo", "crm", "target")
	tests := []struct {
		name    string
		targets map[string]*packs.SourceBackedPack
	}{
		{name: "missing", targets: nil},
		{name: "nil", targets: map[string]*packs.SourceBackedPack{"crm": nil}},
		{name: "extra instead of planned", targets: map[string]*packs.SourceBackedPack{"other": exact}},
		{name: "noncanonical key", targets: map[string]*packs.SourceBackedPack{"CRM": exact}},
		{name: "key name mismatch", targets: map[string]*packs.SourceBackedPack{"crm": testApplyAllSourceRevisionTarget(t, "other", "1.0.0", "official_repo", "crm", "target")}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if _, err := orderApplyAllSourceRevisionAdvanceTuple([]normalizedApplyAllSourceRevisionAdvance{advance}, test.targets); err == nil {
				t.Fatal("order tuple unexpectedly succeeded")
			}
		})
	}
}

func TestOrderApplyAllSourceRevisionAdvanceTupleRejectsPlannedDependencyVersionMismatch(t *testing.T) {
	consumer := testApplyAllSourceRevisionAdvance("consumer", 0)
	provider := testApplyAllSourceRevisionAdvance("provider", 1)
	targets := map[string]*packs.SourceBackedPack{
		"consumer": testApplyAllSourceRevisionTarget(t, "consumer", "1.0.0", "official_repo", "consumer", "target",
			packs.PackDependency{Name: "provider", VersionConstraint: ">=2.0.0"},
		),
		"provider": testApplyAllSourceRevisionTarget(t, "provider", "1.9.9", "official_repo", "provider", "target"),
	}
	_, err := orderApplyAllSourceRevisionAdvanceTuple([]normalizedApplyAllSourceRevisionAdvance{consumer, provider}, targets)
	if err == nil || !strings.Contains(err.Error(), `provider version "1.9.9" does not satisfy ">=2.0.0"`) {
		t.Fatalf("order tuple error = %v, want dependency constraint failure", err)
	}
}

func TestOrderApplyAllSourceRevisionAdvanceTupleRejectsCycle(t *testing.T) {
	t.Run("two packs", func(t *testing.T) {
		a := testApplyAllSourceRevisionAdvance("a", 0)
		b := testApplyAllSourceRevisionAdvance("b", 1)
		targets := map[string]*packs.SourceBackedPack{
			"a": testApplyAllSourceRevisionTarget(t, "a", "1.0.0", "official_repo", "a", "target", packs.PackDependency{Name: "b", VersionConstraint: ">=1.0.0"}),
			"b": testApplyAllSourceRevisionTarget(t, "b", "1.0.0", "official_repo", "b", "target", packs.PackDependency{Name: "a", VersionConstraint: ">=1.0.0"}),
		}
		if _, err := orderApplyAllSourceRevisionAdvanceTuple([]normalizedApplyAllSourceRevisionAdvance{a, b}, targets); err == nil || !strings.Contains(err.Error(), "dependency cycle") {
			t.Fatalf("order tuple error = %v, want dependency cycle", err)
		}
	})
	t.Run("self dependency", func(t *testing.T) {
		a := testApplyAllSourceRevisionAdvance("a", 0)
		targets := map[string]*packs.SourceBackedPack{
			"a": testApplyAllSourceRevisionTarget(t, "a", "1.0.0", "official_repo", "a", "target", packs.PackDependency{Name: "a", VersionConstraint: ">=1.0.0"}),
		}
		if _, err := orderApplyAllSourceRevisionAdvanceTuple([]normalizedApplyAllSourceRevisionAdvance{a}, targets); err == nil || !strings.Contains(err.Error(), "dependency cycle") {
			t.Fatalf("order tuple error = %v, want dependency cycle", err)
		}
	})
}

func testApplyAllSourceRevisionAdvance(name string, index int) normalizedApplyAllSourceRevisionAdvance {
	return normalizedApplyAllSourceRevisionAdvance{
		SourceRevisionAdvanceAuthorization: SourceRevisionAdvanceAuthorization{
			Name:         name,
			Kind:         "official_repo",
			URI:          name,
			FromRevision: "from",
			ToRevision:   "target",
		},
		index: index,
	}
}

func testApplyAllSourceRevisionTarget(
	t *testing.T,
	name string,
	version string,
	kind string,
	uri string,
	revision string,
	dependencies ...packs.PackDependency,
) *packs.SourceBackedPack {
	t.Helper()
	target, err := packs.NewSourceBackedPack(&packs.ResolvedPackSource{
		Kind:     kind,
		URI:      uri,
		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{},
	})
	if err != nil {
		t.Fatalf("new source-backed target %s: %v", name, err)
	}
	return target
}
