package depinject_test

import (
	"errors"
	"fmt"
	"os"
	"testing"

	"github.com/stretchr/testify/require"
	"gotest.tools/v3/golden"

	"cosmossdk.io/depinject"
)

type KVStoreKey struct {
	name string
}

type MsgClientA struct {
	key string
}

type KeeperA struct {
	key  KVStoreKey
	name string
}

type KeeperB struct {
	key        KVStoreKey
	msgClientA MsgClientA
}

type KeeperC struct {
	key        KVStoreKey
	msgClientA MsgClientA
}

type KeeperD struct {
	key KVStoreKey
}

type Handler struct {
	Handle func()
}

func (Handler) IsOnePerModuleType() {}

type Command struct {
	Run func()
}

func (Command) IsManyPerContainerType() {}

func ProvideKVStoreKey(moduleKey depinject.ModuleKey) KVStoreKey {
	return KVStoreKey{name: moduleKey.Name()}
}

func ProvideMsgClientA(key depinject.ModuleKey) MsgClientA {
	return MsgClientA{key.Name()}
}

type ModuleA struct{}

func (ModuleA) Provide(key KVStoreKey, moduleKey depinject.OwnModuleKey) (KeeperA, Handler, Command) {
	return KeeperA{key: key, name: depinject.ModuleKey(moduleKey).Name()}, Handler{}, Command{}
}

type ModuleB struct{}

type BDependencies struct {
	depinject.In

	Key KVStoreKey
	A   MsgClientA
}

type BProvides struct {
	depinject.Out

	KeeperB  KeeperB
	Commands []Command
}

func (ModuleB) Provide(dependencies BDependencies) (BProvides, Handler, error) {
	return BProvides{
		KeeperB: KeeperB{
			key:        dependencies.Key,
			msgClientA: dependencies.A,
		},
		Commands: []Command{{}, {}},
	}, Handler{}, nil
}

type ModuleUnexportedDependency struct{}

func (ModuleUnexportedDependency) Provide(dependencies UnexportedFieldCDependencies) (CProvides, Handler, error) {
	return CProvides{
		KeeperC: KeeperC{
			key:        dependencies.key,
			msgClientA: dependencies.A,
		},
		Commands: []Command{{}, {}},
	}, Handler{}, nil
}

type UnexportedFieldCDependencies struct {
	depinject.In

	key KVStoreKey
	A   MsgClientA
}

type CProvides struct {
	depinject.Out

	KeeperC  KeeperC
	Commands []Command
}

type ModuleUnexportedProvides struct{}

type CDependencies struct {
	depinject.In

	Key KVStoreKey
	A   MsgClientA
}

type UnexportedFieldCProvides struct {
	depinject.Out

	keeperC  KeeperC
	Commands []Command
}

func (ModuleUnexportedProvides) Provide(dependencies CDependencies) (UnexportedFieldCProvides, Handler, error) {
	return UnexportedFieldCProvides{
		keeperC: KeeperC{
			key:        dependencies.Key,
			msgClientA: dependencies.A,
		},
		Commands: []Command{{}, {}},
	}, Handler{}, nil
}

type ModuleD struct{}

type DDependencies struct {
	depinject.In

	Key     KVStoreKey
	KeeperC KeeperC
}

type DProvides struct {
	depinject.Out

	KeeperD  KeeperD
	Commands []Command
}

func (ModuleD) Provide(dependencies DDependencies) (DProvides, Handler, error) {
	return DProvides{
		KeeperD: KeeperD{
			key: dependencies.Key,
		},
		Commands: []Command{{}, {}},
	}, Handler{}, nil
}

func TestUnexportedField(t *testing.T) {
	var (
		handlers map[string]Handler
		commands []Command
		a        KeeperA
		c        KeeperC
		d        KeeperD

		scenarioConfigProvides = depinject.Configs(
			depinject.Provide(ProvideMsgClientA),
			depinject.ProvideInModule("runtime", ProvideKVStoreKey),
			depinject.ProvideInModule("a", ModuleA.Provide),
			depinject.ProvideInModule("c", ModuleUnexportedProvides.Provide),
			depinject.Supply(ModuleA{}, ModuleUnexportedProvides{}),
		)

		scenarioConfigDependency = depinject.Configs(
			depinject.Provide(ProvideMsgClientA),
			depinject.ProvideInModule("runtime", ProvideKVStoreKey),
			depinject.ProvideInModule("a", ModuleA.Provide),
			depinject.ProvideInModule("c", ModuleUnexportedDependency.Provide),
			depinject.Supply(ModuleA{}, ModuleUnexportedDependency{}),
		)

		scenarioConfigProvidesDependency = depinject.Configs(
			depinject.Provide(ProvideMsgClientA),
			depinject.ProvideInModule("runtime", ProvideKVStoreKey),
			depinject.ProvideInModule("a", ModuleA.Provide),
			depinject.ProvideInModule("c", ModuleUnexportedProvides.Provide),
			depinject.ProvideInModule("d", ModuleD.Provide),
			depinject.Supply(ModuleA{}, ModuleUnexportedProvides{}, ModuleD{}),
		)
	)

	require.ErrorContains(t,
		depinject.Inject(
			scenarioConfigProvides,
			&handlers,
			&commands,
			&a,
			&c,
		),
		"depinject.Out struct",
	)

	require.NoError(t,
		depinject.Inject(
			scenarioConfigDependency,
			&handlers,
			&commands,
			&a,
			&c,
		),
	)

	require.ErrorContains(t,
		depinject.Inject(
			scenarioConfigProvidesDependency,
			&handlers,
			&commands,
			&a,
			&c,
			&d,
		),
		"depinject.Out struct",
	)
}

var scenarioConfig = depinject.Configs(
	depinject.Provide(ProvideMsgClientA),
	depinject.ProvideInModule("runtime", ProvideKVStoreKey),
	depinject.ProvideInModule("a", ModuleA.Provide),
	depinject.ProvideInModule("b", ModuleB.Provide),
	depinject.Supply(ModuleA{}, ModuleB{}),
)

func TestScenario(t *testing.T) {
	var (
		handlers map[string]Handler
		commands []Command
		a        KeeperA
		b        KeeperB
	)
	require.NoError(t,
		depinject.Inject(
			scenarioConfig,
			&handlers,
			&commands,
			&a,
			&b,
		))

	require.Len(t, handlers, 2)
	require.Equal(t, Handler{}, handlers["a"])
	require.Equal(t, Handler{}, handlers["b"])
	require.Len(t, commands, 3)
	require.Equal(t, KeeperA{
		key:  KVStoreKey{name: "a"},
		name: "a",
	}, a)
	require.Equal(t, KeeperB{
		key: KVStoreKey{name: "b"},
		msgClientA: MsgClientA{
			key: "b",
		},
	}, b)
}

func TestResolveError(t *testing.T) {
	var x string
	require.Error(t, depinject.Inject(
		depinject.Provide(
			func(x float64) string { return fmt.Sprintf("%f", x) },
			func(x int) float64 { return float64(x) },
			func(x float32) int { return int(x) },
		),
		&x,
	))
}

func TestCyclic(t *testing.T) {
	var x string
	require.Error(t, depinject.Inject(
		depinject.Provide(
			func(x int) float64 { return float64(x) },
			func(x float64) (int, string) { return int(x), "hi" },
		),
		&x,
	))
}

func TestErrorOption(t *testing.T) {
	err := depinject.Inject(depinject.Error(errors.New("an error")))
	require.Error(t, err)
}

func TestTrivial(t *testing.T) {
	require.NoError(t, depinject.Inject(depinject.Configs()))
}

func Provide0() int { return 0 }
func Provide1() int { return 1 }

func TestSimple(t *testing.T) {
	var x int
	require.NoError(t,
		depinject.Inject(
			depinject.Provide(Provide1),
			&x,
		),
	)

	require.Error(t,
		depinject.Inject(
			depinject.Provide(Provide0, Provide1),
			&x,
		),
	)
}

func ProvideModuleScoped0(depinject.ModuleKey) int { return 0 }
func ProvideModuleScoped1(depinject.ModuleKey) int { return 1 }
func ProvideFloat64FromInt(x int) float64          { return float64(x) }
func ProvideFloat32FromInt(x int) float32          { return float32(x) }

func TestModuleScoped(t *testing.T) {
	var x int
	require.Error(t,
		depinject.Inject(
			depinject.Provide(
				ProvideModuleScoped0,
			),
			&x,
		),
	)

	var y float64
	require.Error(t,
		depinject.Inject(
			depinject.Configs(
				depinject.Provide(
					ProvideModuleScoped0,
					Provide1,
				),
				depinject.ProvideInModule("a", ProvideFloat64FromInt),
			),
			&y,
		),
	)

	require.Error(t,
		depinject.Inject(
			depinject.Configs(
				depinject.Provide(
					Provide0,
					ProvideModuleScoped0,
				),
				depinject.ProvideInModule("a", ProvideFloat64FromInt),
			),
			&y,
		),
	)

	require.Error(t,
		depinject.Inject(
			depinject.Configs(
				depinject.Provide(
					ProvideModuleScoped0,
					ProvideModuleScoped1,
				),
				depinject.ProvideInModule("a", ProvideFloat64FromInt),
			),
			&y,
		),
	)

	require.NoError(t,
		depinject.Inject(
			depinject.Configs(
				depinject.Provide(ProvideModuleScoped0),
				depinject.ProvideInModule("a", ProvideFloat64FromInt),
			),
			&y,
		),
	)

	require.Error(t,
		depinject.Inject(
			depinject.Configs(
				depinject.Provide(ProvideModuleScoped0),
				depinject.ProvideInModule("", ProvideFloat64FromInt),
			),
			&y,
		),
	)

	var z float32
	require.NoError(t,
		depinject.Inject(
			depinject.Configs(
				depinject.Provide(ProvideModuleScoped0),
				depinject.ProvideInModule("a",
					ProvideFloat64FromInt,
					ProvideFloat32FromInt,
				),
			),
			&y, &z,
		),
		"use module dep twice",
	)
}

type OnePerModuleInt int

func (OnePerModuleInt) IsOnePerModuleType() {}

func OnePerModuleInt3() OnePerModuleInt { return 3 }
func OnePerModuleInt4() OnePerModuleInt { return 4 }
func CollectOnePerModuleInts(x map[string]OnePerModuleInt) string {
	sum := 0
	for _, v := range x {
		sum += int(v)
	}
	return fmt.Sprintf("%d", sum)
}

func ReturnOnePerModuleMap() map[string]OnePerModuleInt { return nil }

func TestOnePerModule(t *testing.T) {
	var x OnePerModuleInt
	require.Error(t,
		depinject.Inject(depinject.Configs(), &x),
		"bad input type",
	)

	var y map[string]OnePerModuleInt
	var z string
	require.NoError(t,
		depinject.Inject(
			depinject.Configs(
				depinject.ProvideInModule("a", OnePerModuleInt3),
				depinject.ProvideInModule("b", OnePerModuleInt4),
				depinject.Provide(CollectOnePerModuleInts),
			),
			&y,
			&z,
		),
	)

	require.Equal(t, map[string]OnePerModuleInt{
		"a": 3,
		"b": 4,
	}, y)
	require.Equal(t, "7", z)

	var m map[string]OnePerModuleInt
	require.Error(t,
		depinject.Inject(
			depinject.ProvideInModule("a",
				OnePerModuleInt3,
				OnePerModuleInt3,
			),
			&m,
		),
		"duplicate",
	)

	require.Error(t,
		depinject.Inject(
			depinject.Provide(
				OnePerModuleInt3,
			),
			&m,
		),
		"out of scope",
	)

	require.Error(t,
		depinject.Inject(
			depinject.Provide(ReturnOnePerModuleMap),
			&m,
		),
		"bad return type",
	)

	require.NoError(t,
		depinject.Inject(depinject.Configs(), &m),
		"no providers",
	)
}

type ManyPerContainerInt int

func (ManyPerContainerInt) IsManyPerContainerType() {}

func ManyPerContainerInt4() ManyPerContainerInt { return 4 }
func ManyPerContainerInt9() ManyPerContainerInt { return 9 }
func CollectManyPerContainerInts(xs []ManyPerContainerInt) string {
	sum := 0
	for _, x := range xs {
		sum += int(x)
	}
	return fmt.Sprintf("%d", sum)
}

func TestManyPerContainer(t *testing.T) {
	var xs []ManyPerContainerInt
	var sum string
	require.NoError(t,
		depinject.Inject(
			depinject.Provide(
				ManyPerContainerInt4, ManyPerContainerInt9,
				CollectManyPerContainerInts,
			),
			&xs,
			&sum,
		),
	)
	require.Len(t, xs, 2)
	require.Contains(t, xs, ManyPerContainerInt(4))
	require.Contains(t, xs, ManyPerContainerInt(9))
	require.Equal(t, "13", sum)

	var z ManyPerContainerInt
	require.Error(t,
		depinject.Inject(depinject.Provide(ManyPerContainerInt4), &z),
		"bad input type",
	)

	require.NoError(t,
		depinject.Inject(
			depinject.Configs(),
			&xs,
		),
		"no providers",
	)
}

func TestSupply(t *testing.T) {
	var x int
	require.NoError(t,
		depinject.Inject(
			depinject.Supply(3),
			&x,
		),
	)
	require.Equal(t, 3, x)

	require.Error(t,
		depinject.Inject(
			depinject.Configs(
				depinject.Supply(3),
				depinject.Provide(func() int { return 4 }),
			),
			&x,
		),
		"can't supply then provide",
	)

	require.Error(t,
		depinject.Inject(
			depinject.Configs(
				depinject.Supply(3),
				depinject.Provide(func() int { return 4 }),
			),
			&x,
		),
		"can't provide then supply",
	)

	require.Error(t,
		depinject.Inject(
			depinject.Supply(3, 4),
			&x,
		),
		"can't supply twice",
	)
}

type TestInput struct {
	depinject.In

	X int `optional:"true"`
	Y float64
}

type TestOutput struct {
	depinject.Out

	X string
	Y int64
}

func ProvideTestOutput() (TestOutput, error) {
	return TestOutput{X: "A", Y: -10}, nil
}

func ProvideTestOutputErr() (TestOutput, error) {
	return TestOutput{}, errors.New("error")
}

func TestStructArgs(t *testing.T) {
	var input TestInput
	require.Error(t, depinject.Inject(depinject.Configs(), &input))

	require.NoError(t, depinject.Inject(
		depinject.Supply(1.3),
		&input,
	))
	require.Equal(t, 0, input.X)
	require.Equal(t, 1.3, input.Y)

	require.NoError(t, depinject.Inject(
		depinject.Supply(1.3, 1),
		&input,
	))
	require.Equal(t, 1, input.X)
	require.Equal(t, 1.3, input.Y)

	var x string
	var y int64
	require.NoError(t, depinject.Inject(
		depinject.Provide(ProvideTestOutput),
		&x, &y,
	))
	require.Equal(t, "A", x)
	require.Equal(t, int64(-10), y)

	require.Error(t, depinject.Inject(
		depinject.Provide(ProvideTestOutputErr),
		&x,
	))
}

func TestDebugOptions(t *testing.T) {
	var logOut string
	var dotGraph string

	outfile, err := os.CreateTemp("", "out")
	require.NoError(t, err)
	stdout := os.Stdout
	os.Stdout = outfile
	defer func() { os.Stdout = stdout }()
	defer func() {
		err := os.Remove(outfile.Name())
		if err != nil {
			panic(err)
		}
	}()

	graphfile, err := os.CreateTemp("", "graph")
	require.NoError(t, err)
	defer func() {
		err := os.Remove(graphfile.Name())
		if err != nil {
			panic(err)
		}
	}()

	require.NoError(t, depinject.InjectDebug(
		depinject.DebugOptions(
			depinject.Logger(func(s string) {
				logOut += s
			}),
			depinject.Visualizer(func(g string) {
				dotGraph = g
			}),
			depinject.LogVisualizer(),
			depinject.FileVisualizer(graphfile.Name()),
			depinject.StdoutLogger(),
		),
		depinject.Configs(),
	))

	require.Contains(t, logOut, "digraph")
	require.Contains(t, dotGraph, "digraph")

	outfileContents, err := os.ReadFile(outfile.Name())
	require.NoError(t, err)
	require.Contains(t, string(outfileContents), "digraph")

	graphfileContents, err := os.ReadFile(graphfile.Name())
	require.NoError(t, err)
	require.Contains(t, string(graphfileContents), "digraph")
}

func TestGraphAndLogOutput(t *testing.T) {
	var graphOut string
	var b KeeperB
	debugOpts := depinject.DebugOptions(
		depinject.Visualizer(func(dotGraph string) {
			graphOut = dotGraph
		}))
	require.NoError(t, depinject.InjectDebug(debugOpts, scenarioConfig, &b))
	golden.Assert(t, graphOut, "example.dot")

	badConfig := depinject.Configs(
		depinject.ProvideInModule("runtime", ProvideKVStoreKey),
		depinject.ProvideInModule("a", ModuleA.Provide),
		depinject.ProvideInModule("b", ModuleB.Provide),
	)
	require.Error(t, depinject.InjectDebug(debugOpts, badConfig, &b))
	golden.Assert(t, graphOut, "example_error.dot")
}

func TestConditionalDebugging(t *testing.T) {
	logs := ""
	success := false
	conditionalDebugOpt := depinject.DebugOptions(
		depinject.OnError(depinject.Logger(func(s string) {
			logs += s + "\n"
		})),
		depinject.OnSuccess(depinject.DebugCleanup(func() {
			success = true
		})))

	var input TestInput
	require.Error(t, depinject.InjectDebug(
		conditionalDebugOpt,
		depinject.Configs(),
		&input,
	))
	require.Contains(t, logs, `Initializing logger`)
	require.Contains(t, logs, `Registering providers`)
	require.Contains(t, logs, `Registering outputs`)
	require.False(t, success)

	logs = ""
	success = false
	require.NoError(t, depinject.InjectDebug(
		conditionalDebugOpt,
		depinject.Configs(),
	))
	require.Empty(t, logs)
	require.True(t, success)
}

type TestFuncTypesInputs struct {
	depinject.In

	DuckReturner func() Duck `optional:"true"`
}

type smallMallard struct{}

func (smallMallard) quack() {}

func DuckProvider(in TestFuncTypesInputs) Duck {
	if in.DuckReturner != nil {
		return in.DuckReturner()
	}
	return Mallard{}
}

func TestFuncTypes(t *testing.T) {
	var duckReturnerFactory func() Duck
	err := depinject.Inject(
		depinject.Supply(func() Duck { return smallMallard{} }),
		&duckReturnerFactory)
	require.NoError(t, err)
	_, ok := duckReturnerFactory().(smallMallard)
	require.True(t, ok)

	var duck Duck
	err = depinject.Inject(
		depinject.Configs(
			depinject.Supply(func() Duck { return smallMallard{} }),
			depinject.Provide(DuckProvider),
		),
		&duck)
	_, ok = duck.(smallMallard)
	require.True(t, ok)
	require.NoError(t, err)

	err = depinject.Inject(
		depinject.Configs(
			depinject.Provide(DuckProvider),
		),
		&duck)
	_, ok = duck.(Mallard)
	require.True(t, ok)
	require.NoError(t, err)
}
