package logs

import (
	"bytes"
	"context"
	"fmt"
	"testing"

	"github.com/google/shlex"
	"github.com/schmitthub/clawker/internal/cmdutil"
	"github.com/schmitthub/clawker/internal/config"
	configmocks "github.com/schmitthub/clawker/internal/config/mocks"
	"github.com/schmitthub/clawker/internal/docker"
	"github.com/schmitthub/clawker/internal/docker/mocks"
	"github.com/schmitthub/clawker/internal/iostreams"
	"github.com/schmitthub/clawker/internal/logger"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewCmdLogs(t *testing.T) {
	tests := []struct {
		name       string
		input      string
		args       []string
		output     LogsOptions
		wantErr    bool
		wantErrMsg string
	}{
		{
			name:   "single container",
			input:  "",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Tail: "all"},
		},
		{
			name:   "with follow flag",
			input:  "--follow",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Follow: true, Tail: "all"},
		},
		{
			name:   "with shorthand follow flag",
			input:  "-f",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Follow: true, Tail: "all"},
		},
		{
			name:   "with timestamps flag",
			input:  "--timestamps",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Timestamps: true, Tail: "all"},
		},
		{
			name:   "with shorthand timestamps flag",
			input:  "-t",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Timestamps: true, Tail: "all"},
		},
		{
			name:   "with tail flag",
			input:  "--tail 50",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Tail: "50"},
		},
		{
			name:   "with since flag",
			input:  "--since 2024-01-01T00:00:00Z",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Since: "2024-01-01T00:00:00Z", Tail: "all"},
		},
		{
			name:   "with until flag",
			input:  "--until 2024-01-02T00:00:00Z",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Until: "2024-01-02T00:00:00Z", Tail: "all"},
		},
		{
			name:   "with details flag",
			input:  "--details",
			args:   []string{"clawker.myapp.dev"},
			output: LogsOptions{Details: true, Tail: "all"},
		},
		{
			name:   "with agent flag",
			input:  "--agent",
			args:   []string{"dev"},
			output: LogsOptions{Agent: true, Tail: "all"},
		},
		{
			name:       "no container specified",
			input:      "",
			args:       []string{},
			wantErr:    true,
			wantErrMsg: "accepts 1 arg(s), received 0",
		},
		{
			name:       "too many containers",
			input:      "",
			args:       []string{"clawker.myapp.dev", "clawker.myapp.writer"},
			wantErr:    true,
			wantErrMsg: "accepts 1 arg(s), received 2",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			f := &cmdutil.Factory{
				Config: func() (config.Config, error) {
					return configmocks.NewBlankConfig(), nil
				},
			}

			var gotOpts *LogsOptions
			cmd := NewCmdLogs(f, func(_ context.Context, opts *LogsOptions) error {
				gotOpts = opts
				return nil
			})

			// Cobra hack-around for help flag
			cmd.Flags().BoolP("help", "x", false, "")

			// Parse arguments
			argv := tt.args
			if tt.input != "" {
				parsed, err := shlex.Split(tt.input)
				require.NoError(t, err)
				argv = append(parsed, tt.args...)
			}

			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(&bytes.Buffer{})
			cmd.SetErr(&bytes.Buffer{})

			_, err := cmd.ExecuteC()
			if tt.wantErr {
				require.Error(t, err)
				require.Contains(t, err.Error(), tt.wantErrMsg)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, gotOpts)
			require.Equal(t, tt.output.Agent, gotOpts.Agent)
			require.Equal(t, tt.output.Follow, gotOpts.Follow)
			require.Equal(t, tt.output.Timestamps, gotOpts.Timestamps)
			require.Equal(t, tt.output.Details, gotOpts.Details)
			require.Equal(t, tt.output.Since, gotOpts.Since)
			require.Equal(t, tt.output.Until, gotOpts.Until)
			require.Equal(t, tt.output.Tail, gotOpts.Tail)
		})
	}
}

func TestCmdLogs_Properties(t *testing.T) {
	f := &cmdutil.Factory{}
	cmd := NewCmdLogs(f, nil)

	// Test command basics
	require.Equal(t, "logs [CONTAINER]", cmd.Use)
	require.NotEmpty(t, cmd.Short)
	require.NotEmpty(t, cmd.Long)
	require.NotEmpty(t, cmd.Example)
	require.NotNil(t, cmd.RunE)

	// Test flags exist
	require.NotNil(t, cmd.Flags().Lookup("follow"))
	require.NotNil(t, cmd.Flags().Lookup("timestamps"))
	require.NotNil(t, cmd.Flags().Lookup("details"))
	require.NotNil(t, cmd.Flags().Lookup("since"))
	require.NotNil(t, cmd.Flags().Lookup("until"))
	require.NotNil(t, cmd.Flags().Lookup("tail"))

	// Test shorthand flags
	require.NotNil(t, cmd.Flags().ShorthandLookup("f"))
	require.NotNil(t, cmd.Flags().ShorthandLookup("t"))

	// Test default tail
	tail, _ := cmd.Flags().GetString("tail")
	require.Equal(t, "all", tail)
}

// --- Tier 2: Cobra+Factory integration tests ---

func testFactory(t *testing.T, fake *mocks.FakeClient) (*cmdutil.Factory, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
	t.Helper()
	tio, in, out, errOut := iostreams.Test()
	return &cmdutil.Factory{
		IOStreams: tio,
		Logger:    func() (*logger.Logger, error) { return logger.Nop(), nil },
		Client: func(_ context.Context) (*docker.Client, error) {
			return fake.Client, nil
		},
		Config: func() (config.Config, error) {
			return configmocks.NewBlankConfig(), nil
		},
	}, in, out, errOut
}

func TestLogsRun_HappyPath(t *testing.T) {
	fake := mocks.NewFakeClient(configmocks.NewBlankConfig())
	c := mocks.RunningContainerFixture("myapp", "dev")
	fake.SetupFindContainer("clawker.myapp.dev", c)
	fake.SetupContainerLogs("line1\nline2\nline3\n")

	f, in, out, errOut := testFactory(t, fake)
	cmd := NewCmdLogs(f, nil)
	cmd.SetArgs([]string{"clawker.myapp.dev"})
	cmd.SetIn(in)
	cmd.SetOut(out)
	cmd.SetErr(errOut)

	err := cmd.Execute()
	require.NoError(t, err)
	assert.Equal(t, "line1\nline2\nline3\n", out.String())
}

func TestLogsRun_DockerConnectionError(t *testing.T) {
	tio, in, out, errOut := iostreams.Test()
	f := &cmdutil.Factory{
		IOStreams: tio,
		Logger:    func() (*logger.Logger, error) { return logger.Nop(), nil },
		Client: func(_ context.Context) (*docker.Client, error) {
			return nil, fmt.Errorf("cannot connect to Docker daemon")
		},
		Config: func() (config.Config, error) {
			return configmocks.NewBlankConfig(), nil
		},
	}

	cmd := NewCmdLogs(f, nil)
	cmd.SetArgs([]string{"clawker.myapp.dev"})
	cmd.SetIn(in)
	cmd.SetOut(out)
	cmd.SetErr(errOut)

	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "connecting to Docker")
}

func TestLogsRun_ContainerNotFound(t *testing.T) {
	fake := mocks.NewFakeClient(configmocks.NewBlankConfig())
	fake.SetupContainerList() // empty list

	f, in, out, errOut := testFactory(t, fake)
	cmd := NewCmdLogs(f, nil)
	cmd.SetArgs([]string{"clawker.myapp.nonexistent"})
	cmd.SetIn(in)
	cmd.SetOut(out)
	cmd.SetErr(errOut)

	err := cmd.Execute()
	require.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestLogsRun_WithTailFlag(t *testing.T) {
	fake := mocks.NewFakeClient(configmocks.NewBlankConfig())
	c := mocks.RunningContainerFixture("myapp", "dev")
	fake.SetupFindContainer("clawker.myapp.dev", c)
	fake.SetupContainerLogs("last line\n")

	f, in, out, errOut := testFactory(t, fake)
	cmd := NewCmdLogs(f, nil)
	cmd.SetArgs([]string{"--tail", "1", "clawker.myapp.dev"})
	cmd.SetIn(in)
	cmd.SetOut(out)
	cmd.SetErr(errOut)

	err := cmd.Execute()
	require.NoError(t, err)
	assert.Equal(t, "last line\n", out.String())
}
