package service

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
)

func TestBuildCommand(t *testing.T) {
	tests := []struct {
		name        string
		command     string
		wantCmdName string
		wantArgs    []string
		wantErr     bool
	}{
		{
			name:        "pd-ctl command",
			command:     "pd-ctl config show",
			wantCmdName: "sh",
			wantArgs:    []string{"-c", "pd-ctl config show"},
			wantErr:     false,
		},
		{
			name:        "cse-ctl command",
			command:     "cse-ctl --host=localhost:20160 raft region",
			wantCmdName: "sh",
			wantArgs:    []string{"-c", "cse-ctl --host=localhost:20160 raft region"},
			wantErr:     false,
		},
		{
			name:        "echo command",
			command:     "echo hello world",
			wantCmdName: "sh",
			wantArgs:    []string{"-c", "echo hello world"},
			wantErr:     false,
		},
		{
			name:        "command with leading/trailing spaces",
			command:     "  echo test  ",
			wantCmdName: "sh",
			wantArgs:    []string{"-c", "  echo test  "},
			wantErr:     false,
		},
		{
			name:        "unsupported command",
			command:     "rm -rf /",
			wantCmdName: "",
			wantArgs:    nil,
			wantErr:     true,
		},
		{
			name:        "unsupported command - curl",
			command:     "curl http://example.com",
			wantCmdName: "",
			wantArgs:    nil,
			wantErr:     true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmdName, args, err := buildCommand(tt.command)
			if tt.wantErr {
				assert.Error(t, err)
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tt.wantCmdName, cmdName)
			assert.Equal(t, tt.wantArgs, args)
		})
	}
}

func TestExecute(t *testing.T) {
	tests := []struct {
		name           string
		command        string
		wantStdout     string
		wantStderr     string
		wantExitCode   *int32
		wantErrContain string
	}{
		{
			name:         "successful echo command",
			command:      "echo hello",
			wantStdout:   "hello\n",
			wantStderr:   "",
			wantExitCode: int32Ptr(0),
		},
		{
			name:         "echo to stderr",
			command:      "echo error >&2",
			wantStdout:   "",
			wantStderr:   "error\n",
			wantExitCode: int32Ptr(0),
		},
		{
			name:           "unsupported command",
			command:        "unsupported-cmd",
			wantErrContain: "unsupported command",
		},
		{
			name:           "command with non-zero exit code",
			command:        "echo test && exit 42",
			wantStdout:     "",
			wantStderr:     "",
			wantExitCode:   int32Ptr(42),
			wantErrContain: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			stdout, stderr, exitCode, err := execute(ctx, tt.command)

			if tt.wantErrContain != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.wantErrContain)
				return
			}

			if tt.wantExitCode != nil && *tt.wantExitCode != 0 {
				// Non-zero exit code is returned without error
				require.NoError(t, err)
				assert.Equal(t, *tt.wantExitCode, *exitCode)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.wantStdout, stdout)
			assert.Equal(t, tt.wantStderr, stderr)
			if tt.wantExitCode != nil {
				require.NotNil(t, exitCode)
				assert.Equal(t, *tt.wantExitCode, *exitCode)
			}
		})
	}
}

func TestAgentService_ExecuteCommand(t *testing.T) {
	tests := []struct {
		name    string
		req     *apiv1.ExecuteCommandReq
		wantErr bool
	}{
		{
			name: "valid echo command",
			req: &apiv1.ExecuteCommandReq{
				Command: "echo test",
			},
			wantErr: false,
		},
		{
			name: "invalid command",
			req: &apiv1.ExecuteCommandReq{
				Command: "invalid-command",
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &AgentService{}
			ctx := context.Background()
			resp, err := s.ExecuteCommand(ctx, tt.req)

			if tt.wantErr {
				assert.Error(t, err)
				return
			}

			require.NoError(t, err)
			assert.NotNil(t, resp)
			assert.NotNil(t, resp.ExitCode)
		})
	}
}

func int32Ptr(i int32) *int32 {
	return &i
}
