package command

import (
	"bytes"
	"context"
	"io"
	"strings"
	"testing"

	"github.com/docker/cli/cli/streams"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/spf13/cobra"
	"go.opentelemetry.io/otel/attribute"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

func setupCobraCommands() (*cobra.Command, *cobra.Command, *cobra.Command) {
	rootCmd := &cobra.Command{
		Use: "root [OPTIONS] COMMAND [ARG...]",
	}
	childCmd := &cobra.Command{
		Use: "child [OPTIONS] COMMAND [ARG...]",
	}
	grandchildCmd := &cobra.Command{
		Use: "grandchild [OPTIONS] COMMAND [ARG...]",
	}
	childCmd.AddCommand(grandchildCmd)
	rootCmd.AddCommand(childCmd)

	return rootCmd, childCmd, grandchildCmd
}

func TestGetFullCommandName(t *testing.T) {
	rootCmd, childCmd, grandchildCmd := setupCobraCommands()

	t.Parallel()

	for _, tc := range []struct {
		testName string
		cmd      *cobra.Command
		expected string
	}{
		{
			testName: "rootCmd",
			cmd:      rootCmd,
			expected: "root",
		},
		{
			testName: "childCmd",
			cmd:      childCmd,
			expected: "root child",
		},
		{
			testName: "grandChild",
			cmd:      grandchildCmd,
			expected: "root child grandchild",
		},
	} {
		t.Run(tc.testName, func(t *testing.T) {
			t.Parallel()
			actual := getFullCommandName(tc.cmd)
			assert.Equal(t, actual, tc.expected)
		})
	}
}

func TestGetCommandName(t *testing.T) {
	rootCmd, childCmd, grandchildCmd := setupCobraCommands()

	t.Parallel()

	for _, tc := range []struct {
		testName string
		cmd      *cobra.Command
		expected string
	}{
		{
			testName: "rootCmd",
			cmd:      rootCmd,
			expected: "",
		},
		{
			testName: "childCmd",
			cmd:      childCmd,
			expected: "child",
		},
		{
			testName: "grandchildCmd",
			cmd:      grandchildCmd,
			expected: "child grandchild",
		},
	} {
		t.Run(tc.testName, func(t *testing.T) {
			t.Parallel()
			actual := getCommandName(tc.cmd)
			assert.Equal(t, actual, tc.expected)
		})
	}
}

func TestStdioAttributes(t *testing.T) {
	outBuffer := new(bytes.Buffer)
	errBuffer := new(bytes.Buffer)
	t.Parallel()
	for _, tc := range []struct {
		test      string
		stdinTty  bool
		stdoutTty bool
		// TODO(laurazard): test stderr
		expected []attribute.KeyValue
	}{
		{
			test: "",
			expected: []attribute.KeyValue{
				attribute.Bool("command.stdin.isatty", false),
				attribute.Bool("command.stdout.isatty", false),
				attribute.Bool("command.stderr.isatty", false),
			},
		},
		{
			test:      "",
			stdinTty:  true,
			stdoutTty: true,
			expected: []attribute.KeyValue{
				attribute.Bool("command.stdin.isatty", true),
				attribute.Bool("command.stdout.isatty", true),
				attribute.Bool("command.stderr.isatty", false),
			},
		},
	} {
		t.Run(tc.test, func(t *testing.T) {
			t.Parallel()
			cli := &DockerCli{
				in:  streams.NewIn(io.NopCloser(strings.NewReader(""))),
				out: streams.NewOut(outBuffer),
				err: streams.NewOut(errBuffer),
			}
			cli.In().SetIsTerminal(tc.stdinTty)
			cli.Out().SetIsTerminal(tc.stdoutTty)
			actual := stdioAttributes(cli)

			assert.Check(t, is.DeepEqual(actual, tc.expected, cmpopts.EquateComparable(attribute.Value{})))
		})
	}
}

func TestAttributesFromError(t *testing.T) {
	t.Parallel()

	for _, tc := range []struct {
		testName string
		err      error
		expected []attribute.KeyValue
	}{
		{
			testName: "no error",
			err:      nil,
			expected: []attribute.KeyValue{
				attribute.Int("command.status.code", 0),
			},
		},
		{
			testName: "non-0 exit code",
			err:      statusError{StatusCode: 127},
			expected: []attribute.KeyValue{
				attribute.String("command.error.type", "generic"),
				attribute.Int("command.status.code", 127),
			},
		},
		{
			testName: "canceled",
			err:      context.Canceled,
			expected: []attribute.KeyValue{
				attribute.String("command.error.type", "canceled"),
				attribute.Int("command.status.code", 1),
			},
		},
	} {
		t.Run(tc.testName, func(t *testing.T) {
			t.Parallel()
			actual := attributesFromError(tc.err)
			assert.Check(t, is.DeepEqual(actual, tc.expected, cmpopts.EquateComparable(attribute.Value{})))
		})
	}
}
