package kit

import (
	"bytes"
	"context"
	"flag"
	"strings"
	"testing"

	"github.com/multiformats/go-multiaddr"
	"github.com/stretchr/testify/require"
	lcli "github.com/urfave/cli/v2"

	"github.com/filecoin-project/lotus/api"
)

type MockCLI struct {
	t    *testing.T
	cmds []*lcli.Command
	cctx *lcli.Context
	out  *bytes.Buffer
}

func NewMockCLI(ctx context.Context, t *testing.T, cmds []*lcli.Command, nodeType api.NodeType) *MockCLI {
	// Create a CLI App with an --api-url flag so that we can specify which node
	// the command should be executed against
	app := &lcli.App{
		Flags: []lcli.Flag{
			&lcli.StringFlag{
				Name:   "api-url",
				Hidden: true,
			},
		},
		Commands: cmds,
	}
	// Set node type
	api.RunningNodeType = nodeType

	var out bytes.Buffer
	app.Writer = &out
	app.Setup()

	cctx := lcli.NewContext(app, &flag.FlagSet{}, nil)
	cctx.Context = ctx
	return &MockCLI{t: t, cmds: cmds, cctx: cctx, out: &out}
}

func (c *MockCLI) Client(addr multiaddr.Multiaddr) *MockCLIClient {
	return &MockCLIClient{t: c.t, cmds: c.cmds, addr: addr, cctx: c.cctx, out: c.out}
}

// MockCLIClient runs commands against a particular node
type MockCLIClient struct {
	t    *testing.T
	cmds []*lcli.Command
	addr multiaddr.Multiaddr
	cctx *lcli.Context
	out  *bytes.Buffer
}

func (c *MockCLIClient) RunCmd(input ...string) string {
	out, err := c.RunCmdRaw(input...)
	require.NoError(c.t, err, "output:\n%s", out)

	return out
}

// Given an input, find the corresponding command or sub-command.
// eg "paych add-funds"
func (c *MockCLIClient) cmdByNameSub(input []string) (*lcli.Command, []string) {
	name := input[0]
	for _, cmd := range c.cmds {
		if cmd.Name == name {
			return c.findSubcommand(cmd, input[1:])
		}
	}
	return nil, []string{}
}

func (c *MockCLIClient) findSubcommand(cmd *lcli.Command, input []string) (*lcli.Command, []string) {
	// If there are no sub-commands, return the current command
	if len(cmd.Subcommands) == 0 {
		return cmd, input
	}

	// Check each sub-command for a match against the name
	subName := input[0]
	for _, subCmd := range cmd.Subcommands {
		if subCmd.Name == subName {
			// Found a match, recursively search for sub-commands
			return c.findSubcommand(subCmd, input[1:])
		}
	}
	return nil, []string{}
}

func (c *MockCLIClient) RunCmdRaw(input ...string) (string, error) {
	cmd, input := c.cmdByNameSub(input)
	if cmd == nil {
		panic("Could not find command " + input[0] + " " + input[1])
	}

	// prepend --api-url=<node api listener address>
	apiFlag := "--api-url=" + c.addr.String()
	input = append([]string{apiFlag}, input...)

	fs := c.flagSet(cmd)
	err := fs.Parse(input)
	require.NoError(c.t, err)

	err = cmd.Action(lcli.NewContext(c.cctx.App, fs, c.cctx))

	// Get the output
	str := strings.TrimSpace(c.out.String())
	c.out.Reset()
	return str, err
}

func (c *MockCLIClient) flagSet(cmd *lcli.Command) *flag.FlagSet {
	// Apply app level flags (so we can process --api-url flag)
	fs := &flag.FlagSet{}
	for _, f := range c.cctx.App.Flags {
		err := f.Apply(fs)
		if err != nil {
			c.t.Fatal(err)
		}
	}
	// Apply command level flags
	for _, f := range cmd.Flags {
		err := f.Apply(fs)
		if err != nil {
			c.t.Fatal(err)
		}
	}
	return fs
}

func (c *MockCLIClient) RunInteractiveCmd(cmd []string, interactive []string) string {
	c.toStdin(strings.Join(interactive, "\n") + "\n")
	return c.RunCmd(cmd...)
}

func (c *MockCLIClient) toStdin(s string) {
	c.cctx.App.Metadata["stdin"] = bytes.NewBufferString(s)
}