// Copyright 2024 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package blueprint

import (
	"fmt"
	"regexp"
	"slices"
	"strings"
	"testing"
)

func testTransitionCommon(bp string, neverFar bool, ctxHook func(*Context)) (*Context, []error) {
	ctx := newContext()
	ctx.MockFileSystem(map[string][]byte{
		"Android.bp": []byte(bp),
	})

	ctx.RegisterBottomUpMutator("deps", depsMutator)
	handle := ctx.RegisterTransitionMutator("transition", transitionTestMutator{})
	if neverFar {
		handle.NeverFar()
	}
	ctx.RegisterBottomUpMutator("post_transition_deps", postTransitionDepsMutator).UsesReverseDependencies()

	ctx.RegisterModuleType("transition_module", newTransitionModule)

	if ctxHook != nil {
		ctxHook(ctx)
	}

	_, errs := ctx.ParseBlueprintsFiles("Android.bp", nil)
	if len(errs) > 0 {
		return nil, errs
	}

	_, errs = ctx.ResolveDependencies(nil)
	if len(errs) > 0 {
		return nil, errs
	}

	return ctx, nil
}

func testTransition(bp string) (*Context, []error) {
	return testTransitionCommon(bp, false, nil)
}

func testTransitionNeverFar(bp string) (*Context, []error) {
	return testTransitionCommon(bp, true, nil)
}

func testTransitionAllowMissingDeps(bp string) (*Context, []error) {
	return testTransitionCommon(bp, false, func(ctx *Context) {
		ctx.SetAllowMissingDependencies(true)
	})
}

func assertNoErrors(t *testing.T, errs []error) {
	t.Helper()
	if len(errs) > 0 {
		t.Errorf("unexpected errors:")
		for _, err := range errs {
			t.Errorf("  %s", err)
		}
		t.FailNow()
	}
}

func assertOneErrorMatches(t *testing.T, errs []error, re string) {
	t.Helper()
	if len(errs) == 0 {
		t.Fatalf("expected 1 error, but found 0")
	}
	if len(errs) > 1 {
		t.Errorf("expected exactly 1 error, but found:")
		for _, err := range errs {
			t.Errorf("  %s", err)
		}
		t.FailNow()
	}
	if match, err := regexp.MatchString(re, errs[0].Error()); err != nil {
		t.Fatal(err.Error())
	} else if !match {
		t.Fatalf("expected error matching %q, but got: %q", re, errs[0].Error())
	}
}

const testTransitionBp = `
			transition_module {
			    name: "A",
			    deps: ["B", "C"],
				split: ["b", "a"],
			}

			transition_module {
				name: "B",
				deps: ["C"],
				outgoing: "c",
				%s
			}

			transition_module {
				name: "C",
				deps: ["D"],
			}

			transition_module {
				name: "D",
				incoming: "d",
				deps: ["E"],
			}

			transition_module {
				name: "E",
			}

			transition_module {
				name: "F",
				incoming: "",
			}

			transition_module {
				name: "G",
				outgoing: "h",
				%s
			}

			transition_module {
				name: "H",
				split: ["h"],
			}
		`

func getTransitionModule(ctx *Context, name, variant string) *transitionModule {
	group := ctx.moduleGroupFromName(name, nil)
	module := group.moduleByVariantName(variant)
	return module.logicModule.(*transitionModule)
}

func checkTransitionVariants(t *testing.T, ctx *Context, name string, expectedVariants []string) {
	t.Helper()
	group := ctx.moduleGroupFromName(name, nil)
	var gotVariants []string
	for _, module := range group.modules {
		gotVariants = append(gotVariants, module.variant.variations.get("transition"))
	}
	if !slices.Equal(expectedVariants, gotVariants) {
		t.Errorf("expected variants of %q to be %q, got %q", name, expectedVariants, gotVariants)
	}
}

func checkTransitionDeps(t *testing.T, ctx *Context, m Module, expected ...string) {
	t.Helper()
	var got []string
	ctx.VisitDirectDeps(m, func(m Module) {
		got = append(got, ctx.ModuleName(m)+"("+ctx.ModuleSubDir(m)+")")
	})
	if !slices.Equal(got, expected) {
		t.Errorf("unexpected %q dependencies, got %q expected %q",
			ctx.ModuleName(m), got, expected)
	}
}

func checkTransitionMutate(t *testing.T, m *transitionModule, variant string) {
	t.Helper()
	if m.properties.Mutated != variant {
		t.Errorf("unexpected mutated property in %q, expected %q got %q", m.Name(), variant, m.properties.Mutated)
	}
}

func TestTransition(t *testing.T) {
	ctx, errs := testTransition(fmt.Sprintf(testTransitionBp, "", ""))
	assertNoErrors(t, errs)

	// Module A uses Split to create a and b variants
	checkTransitionVariants(t, ctx, "A", []string{"b", "a"})
	// Module B inherits a and b variants from A
	checkTransitionVariants(t, ctx, "B", []string{"", "a", "b"})
	// Module C inherits a and b variants from A, but gets an outgoing c variant from B
	checkTransitionVariants(t, ctx, "C", []string{"", "a", "b", "c"})
	// Module D always has incoming variant d
	checkTransitionVariants(t, ctx, "D", []string{"", "d"})
	// Module E inherits d from D
	checkTransitionVariants(t, ctx, "E", []string{"", "d"})
	// Module F is untouched
	checkTransitionVariants(t, ctx, "F", []string{""})

	A_a := getTransitionModule(ctx, "A", "a")
	A_b := getTransitionModule(ctx, "A", "b")
	B_a := getTransitionModule(ctx, "B", "a")
	B_b := getTransitionModule(ctx, "B", "b")
	C_a := getTransitionModule(ctx, "C", "a")
	C_b := getTransitionModule(ctx, "C", "b")
	C_c := getTransitionModule(ctx, "C", "c")
	D_d := getTransitionModule(ctx, "D", "d")
	E_d := getTransitionModule(ctx, "E", "d")
	F := getTransitionModule(ctx, "F", "")
	G := getTransitionModule(ctx, "G", "")
	H_h := getTransitionModule(ctx, "H", "h")

	checkTransitionDeps(t, ctx, A_a, "B(a)", "C(a)")
	checkTransitionDeps(t, ctx, A_b, "B(b)", "C(b)")
	checkTransitionDeps(t, ctx, B_a, "C(c)")
	checkTransitionDeps(t, ctx, B_b, "C(c)")
	checkTransitionDeps(t, ctx, C_a, "D(d)")
	checkTransitionDeps(t, ctx, C_b, "D(d)")
	checkTransitionDeps(t, ctx, C_c, "D(d)")
	checkTransitionDeps(t, ctx, D_d, "E(d)")
	checkTransitionDeps(t, ctx, E_d)
	checkTransitionDeps(t, ctx, F)
	checkTransitionDeps(t, ctx, G)
	checkTransitionDeps(t, ctx, H_h)

	checkTransitionMutate(t, A_a, "a")
	checkTransitionMutate(t, A_b, "b")
	checkTransitionMutate(t, B_a, "a")
	checkTransitionMutate(t, B_b, "b")
	checkTransitionMutate(t, C_a, "a")
	checkTransitionMutate(t, C_b, "b")
	checkTransitionMutate(t, C_c, "c")
	checkTransitionMutate(t, D_d, "d")
	checkTransitionMutate(t, E_d, "d")
	checkTransitionMutate(t, F, "")
	checkTransitionMutate(t, G, "")
	checkTransitionMutate(t, H_h, "h")
}

func TestPostTransitionDeps(t *testing.T) {
	ctx, errs := testTransition(fmt.Sprintf(testTransitionBp,
		`post_transition_deps: ["C", "D:late", "E:d", "F"],`,
		`post_transition_deps: ["H"],`))
	assertNoErrors(t, errs)

	// Module A uses Split to create a and b variants
	checkTransitionVariants(t, ctx, "A", []string{"b", "a"})
	// Module B inherits a and b variants from A
	checkTransitionVariants(t, ctx, "B", []string{"", "a", "b"})
	// Module C inherits a and b variants from A, but gets an outgoing c variant from B
	checkTransitionVariants(t, ctx, "C", []string{"", "a", "b", "c"})
	// Module D always has incoming variant d
	checkTransitionVariants(t, ctx, "D", []string{"", "d"})
	// Module E inherits d from D
	checkTransitionVariants(t, ctx, "E", []string{"", "d"})
	// Module F is untouched
	checkTransitionVariants(t, ctx, "F", []string{""})

	A_a := getTransitionModule(ctx, "A", "a")
	A_b := getTransitionModule(ctx, "A", "b")
	B_a := getTransitionModule(ctx, "B", "a")
	B_b := getTransitionModule(ctx, "B", "b")
	C_a := getTransitionModule(ctx, "C", "a")
	C_b := getTransitionModule(ctx, "C", "b")
	C_c := getTransitionModule(ctx, "C", "c")
	D_d := getTransitionModule(ctx, "D", "d")
	E_d := getTransitionModule(ctx, "E", "d")
	F := getTransitionModule(ctx, "F", "")
	G := getTransitionModule(ctx, "G", "")
	H_h := getTransitionModule(ctx, "H", "h")

	checkTransitionDeps(t, ctx, A_a, "B(a)", "C(a)")
	checkTransitionDeps(t, ctx, A_b, "B(b)", "C(b)")
	// Verify post-mutator dependencies added to B.  The first C(c) is a pre-mutator dependency.
	//  C(c) was added by C and rewritten by OutgoingTransition on B
	//  D(d) was added by D:late and rewritten by IncomingTransition on D
	//  E(d) was added by E:d
	//  F() was added by F and rewritten OutgoingTransition on B and then IncomingTransition on F
	checkTransitionDeps(t, ctx, B_a, "C(c)", "C(c)", "D(d)", "E(d)", "F()")
	checkTransitionDeps(t, ctx, B_b, "C(c)", "C(c)", "D(d)", "E(d)", "F()")
	checkTransitionDeps(t, ctx, C_a, "D(d)")
	checkTransitionDeps(t, ctx, C_b, "D(d)")
	checkTransitionDeps(t, ctx, C_c, "D(d)")
	checkTransitionDeps(t, ctx, D_d, "E(d)")
	checkTransitionDeps(t, ctx, E_d)
	checkTransitionDeps(t, ctx, F)
	checkTransitionDeps(t, ctx, G, "H(h)")
	checkTransitionDeps(t, ctx, H_h)

	checkTransitionMutate(t, A_a, "a")
	checkTransitionMutate(t, A_b, "b")
	checkTransitionMutate(t, B_a, "a")
	checkTransitionMutate(t, B_b, "b")
	checkTransitionMutate(t, C_a, "a")
	checkTransitionMutate(t, C_b, "b")
	checkTransitionMutate(t, C_c, "c")
	checkTransitionMutate(t, D_d, "d")
	checkTransitionMutate(t, E_d, "d")
	checkTransitionMutate(t, F, "")
	checkTransitionMutate(t, G, "")
	checkTransitionMutate(t, H_h, "h")
}

func TestPostTransitionReverseDeps(t *testing.T) {
	ctx, errs := testTransition(`
		transition_module {
			name: "A",
			split: ["a1", "a2"],
		}

		transition_module {
			name: "B",
			split: ["a1", "a2"],
			post_transition_reverse_deps: ["A"],
		}
	`)
	assertNoErrors(t, errs)

	checkTransitionVariants(t, ctx, "A", []string{"a1", "a2"})
	checkTransitionVariants(t, ctx, "B", []string{"a1", "a2"})

	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "A", "a1"), "B(a1)")
	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "A", "a2"), "B(a2)")
	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "B", "a1"))
	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "B", "a2"))
}

func TestPostTransitionReverseVariationDeps(t *testing.T) {
	ctx, errs := testTransition(`
		transition_module {
			name: "A",
			split: ["a"],
		}

		transition_module {
			name: "B",
			split: ["b"],
			post_transition_reverse_variation_deps: ["A(a)"],
		}
	`)
	assertNoErrors(t, errs)

	checkTransitionVariants(t, ctx, "A", []string{"a"})
	checkTransitionVariants(t, ctx, "B", []string{"b"})

	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "A", "a"), "B(b)")
	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "B", "b"))
}

func TestFarVariationDep(t *testing.T) {
	ctx, errs := testTransition(`
		transition_module {
			name: "A",
			split: ["a"],
			deps: ["B"],
		}
		transition_module {
			name: "B",
			split: ["", "a"],
		}
		transition_module {
			name: "C",
			split: ["c"],
			post_transition_far_deps: ["D"],
		}
		transition_module {
			name: "D",
			split: ["", "c"],
		}
	`)
	assertNoErrors(t, errs)

	checkTransitionVariants(t, ctx, "A", []string{"a"})
	checkTransitionVariants(t, ctx, "B", []string{"", "a"})
	checkTransitionVariants(t, ctx, "C", []string{"c"})
	checkTransitionVariants(t, ctx, "D", []string{"", "c"})

	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "A", "a"), "B(a)")
	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "C", "c"), "D()")
}

func TestNeverFarFarVariationDep(t *testing.T) {
	ctx, errs := testTransitionNeverFar(`
		transition_module {
			name: "A",
			split: ["a"],
			deps: ["B"],
		}
		transition_module {
			name: "B",
			split: ["", "a"],
		}
		transition_module {
			name: "C",
			split: ["c"],
			post_transition_far_deps: ["D"],
		}
		transition_module {
			name: "D",
			split: ["", "c"],
		}
	`)
	assertNoErrors(t, errs)

	checkTransitionVariants(t, ctx, "A", []string{"a"})
	checkTransitionVariants(t, ctx, "B", []string{"", "a"})
	checkTransitionVariants(t, ctx, "C", []string{"c"})
	checkTransitionVariants(t, ctx, "D", []string{"", "c"})

	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "A", "a"), "B(a)")
	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "C", "c"), "D(c)")
}

func TestPostTransitionReverseDepsErrorOnMissingDep(t *testing.T) {
	_, errs := testTransition(`
		transition_module {
			name: "A",
			split: ["a"],
		}

		transition_module {
			name: "B",
			split: ["b"],
			post_transition_reverse_deps: ["A"],
		}
	`)
	assertOneErrorMatches(t, errs, `reverse dependency "A" of "B" missing variant:\s*transition:b\s*available variants:\s*transition:a`)
}

func TestErrorInIncomingTransition(t *testing.T) {
	_, errs := testTransition(`
		transition_module {
			name: "A",
			split: ["a"],
			deps: ["B"],
		}
		transition_module {
			name: "B",
			split: ["a"],
			incoming_transition_error: "my incoming transition error",
		}
	`)
	assertOneErrorMatches(t, errs, "my incoming transition error")
}

func TestErrorInOutgoingTransition(t *testing.T) {
	_, errs := testTransition(`
		transition_module {
			name: "A",
			split: ["a"],
			deps: ["B"],
			outgoing_transition_error: "my outgoing transition error",
		}
		transition_module {
			name: "B",
			split: ["a"],
		}
	`)
	assertOneErrorMatches(t, errs, "my outgoing transition error")
}

func TestPostTransitionReverseDepsAllowMissingDeps(t *testing.T) {
	_, errs := testTransitionAllowMissingDeps(`
		transition_module {
			name: "A",
			split: ["a"],
		}

		transition_module {
			name: "B",
			split: ["b"],
			post_transition_reverse_deps: ["A"],
		}
	`)
	assertNoErrors(t, errs)
}

func TestPostTransitionDepsMissingVariant(t *testing.T) {
	// TODO: eventually this will create the missing variant on demand
	_, errs := testTransition(fmt.Sprintf(testTransitionBp,
		`post_transition_deps: ["E:missing"],`, ""))
	expectedError := `Android.bp:8:4: dependency "E" of "B" missing variant:
  transition:missing
available variants:
  <empty variant>
  transition:d`
	if len(errs) != 1 || errs[0].Error() != expectedError {
		t.Errorf("expected error %q, got %q", expectedError, errs)
	}
}

func TestIsAddingDependency(t *testing.T) {
	ctx, errs := testTransition(`
		transition_module {
			name: "A",
			split: ["a1"],
			deps: ["C"],
		}

		transition_module {
			name: "B",
			split: ["b1"],
			post_transition_deps: ["C"],
		}

		transition_module {
			name: "C",
			split: ["c1", "c2"],
			incoming: "c1",
			post_transition_incoming: "c2",
		}
	`)
	assertNoErrors(t, errs)

	checkTransitionVariants(t, ctx, "A", []string{"a1"})
	checkTransitionVariants(t, ctx, "B", []string{"b1"})
	checkTransitionVariants(t, ctx, "C", []string{"c1", "c2"})

	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "A", "a1"), "C(c1)")
	checkTransitionDeps(t, ctx, getTransitionModule(ctx, "B", "b1"), "C(c2)")
}

type transitionTestMutator struct{}

func (transitionTestMutator) TransitionInfoFromVariation(s string) TransitionInfo {
	return testTransitionInfo(s)
}

type testTransitionInfo string

func (t testTransitionInfo) Variation() string {
	return string(t)
}

func (transitionTestMutator) Split(ctx BaseModuleContext) []TransitionInfo {
	if split := ctx.Module().(*transitionModule).properties.Split; len(split) > 0 {
		var transitionInfos []TransitionInfo
		for _, s := range split {
			transitionInfos = append(transitionInfos, testTransitionInfo(s))
		}
		return transitionInfos
	}
	return []TransitionInfo{testTransitionInfo("")}
}

func (transitionTestMutator) OutgoingTransition(ctx OutgoingTransitionContext, sourceVariation TransitionInfo) TransitionInfo {
	if err := ctx.Module().(*transitionModule).properties.Outgoing_transition_error; err != nil {
		ctx.ModuleErrorf("Error: %s", *err)
	}
	if outgoing := ctx.Module().(*transitionModule).properties.Outgoing; outgoing != nil {
		return testTransitionInfo(*outgoing)
	}
	return sourceVariation
}

func (transitionTestMutator) IncomingTransition(ctx IncomingTransitionContext, incomingVariation TransitionInfo) TransitionInfo {
	if err := ctx.Module().(*transitionModule).properties.Incoming_transition_error; err != nil {
		ctx.ModuleErrorf("Error: %s", *err)
	}
	if ctx.IsAddingDependency() {
		if incoming := ctx.Module().(*transitionModule).properties.Post_transition_incoming; incoming != nil {
			return testTransitionInfo(*incoming)
		}
	}
	if incoming := ctx.Module().(*transitionModule).properties.Incoming; incoming != nil {
		return testTransitionInfo(*incoming)
	}
	return incomingVariation
}

func (transitionTestMutator) Mutate(ctx BottomUpMutatorContext, variation TransitionInfo) {
	if variation != nil {
		ctx.Module().(*transitionModule).properties.Mutated = variation.Variation()
	}
}

type transitionModule struct {
	ModuleBase
	SimpleName
	properties struct {
		Deps                                   []string
		Post_transition_deps                   []string
		Post_transition_far_deps               []string
		Post_transition_reverse_deps           []string
		Post_transition_reverse_variation_deps []string
		Split                                  []string
		Outgoing                               *string
		Incoming                               *string
		Post_transition_incoming               *string
		Outgoing_transition_error              *string
		Incoming_transition_error              *string

		Mutated string `blueprint:"mutated"`
	}
}

func newTransitionModule() (Module, []interface{}) {
	m := &transitionModule{}
	return m, []interface{}{&m.properties, &m.SimpleName.Properties}
}

func (f *transitionModule) GenerateBuildActions(ModuleContext) {
}

func (f *transitionModule) Deps() []string {
	return f.properties.Deps
}

func (f *transitionModule) IgnoreDeps() []string {
	return nil
}

var nameAndVariantRegexp = regexp.MustCompile(`([a-zA-Z0-9_]+)\(([a-zA-Z0-9_]+)\)`)

func postTransitionDepsMutator(mctx BottomUpMutatorContext) {
	if m, ok := mctx.Module().(*transitionModule); ok {
		for _, dep := range m.properties.Post_transition_deps {
			module, variation, _ := strings.Cut(dep, ":")
			var variations []Variation
			if variation != "" {
				variations = append(variations, Variation{"transition", variation})
			}
			mctx.AddVariationDependencies(variations, walkerDepsTag{follow: true}, module)
		}
		for _, dep := range m.properties.Post_transition_far_deps {
			mctx.AddFarVariationDependencies(nil, walkerDepsTag{follow: true}, dep)
		}
		for _, dep := range m.properties.Post_transition_reverse_deps {
			mctx.AddReverseDependency(m, walkerDepsTag{follow: true}, dep)
		}
		for _, dep := range m.properties.Post_transition_reverse_variation_deps {
			match := nameAndVariantRegexp.FindStringSubmatch(dep)
			if len(match) == 0 || match[0] != dep {
				panic(fmt.Sprintf("Invalid Post_transition_reverse_variation_deps: %q. Expected module_name(variant)", dep))
			}
			mctx.AddReverseVariationDependency([]Variation{
				{Mutator: "transition", Variation: match[2]},
			}, walkerDepsTag{follow: true}, match[1])
		}
	}
}
