/*
 * Copyright (c) 2025 Bocloud Technologies Co., Ltd.
 * installer is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain n copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

package command

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	agentv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkeagent/v1beta1"
	bkenode "gopkg.openfuyao.cn/cluster-api-provider-bke/common/cluster/node"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils"
)

const (
	numC0       = 0
	numC1       = 1
	numC2       = 2
	numC3       = 3
	testNS      = "test-ns"
	testClName  = "test-cluster"
	testCmdName = "test-command"
	testNodeIP  = "192.168.1.1"
	testNodeIP2 = "192.168.1.2"
	testHost    = "test-node"
	cmdTimeout  = 5 * time.Minute
	cmdInterval = 2 * time.Second
)

func TestGenerateBkeConfigStr(t *testing.T) {
	tests := []struct {
		name      string
		namespace string
		config    string
		expected  string
	}{
		{
			name:      "Normal case",
			namespace: testNS,
			config:    testClName,
			expected:  "bkeConfig=test-ns:test-cluster",
		},
		{
			name:      "Empty namespace",
			namespace: "",
			config:    testClName,
			expected:  "bkeConfig=:test-cluster",
		},
		{
			name:      "Empty config",
			namespace: testNS,
			config:    "",
			expected:  "bkeConfig=test-ns:",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := GenerateBkeConfigStr(tt.namespace, tt.config)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestGenerateDefaultCommandSpec(t *testing.T) {
	spec := GenerateDefaultCommandSpec()

	assert.NotNil(t, spec)
	assert.Equal(t, DefaultBackoffLimit, spec.BackoffLimit)
	assert.Equal(t, DefaultActiveDeadlineSecond, spec.ActiveDeadlineSecond)
	assert.Equal(t, DefaultTTLSecondsAfterFinished, spec.TTLSecondsAfterFinished)
	assert.NotNil(t, spec.NodeSelector)
	assert.False(t, spec.Suspend)
}

func TestValidateCommand(t *testing.T) {
	tests := []struct {
		name    string
		cmd     *agentv1beta1.Command
		wantErr bool
	}{
		{
			name: "Valid with node name",
			cmd: &agentv1beta1.Command{
				Spec: agentv1beta1.CommandSpec{
					NodeName: testNodeIP,
				},
			},
			wantErr: false,
		},
		{
			name: "Valid with node selector",
			cmd: &agentv1beta1.Command{
				Spec: agentv1beta1.CommandSpec{
					NodeSelector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							"test": "value",
						},
					},
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidateCommand(tt.cmd)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestCheckCommandStatus(t *testing.T) {
	tests := []struct {
		name           string
		cmd            *agentv1beta1.Command
		expectComplete bool
	}{
		{
			name: "Nil status",
			cmd: &agentv1beta1.Command{
				Status: nil,
			},
			expectComplete: false,
		},
		{
			name: "Suspended command",
			cmd: &agentv1beta1.Command{
				Spec: agentv1beta1.CommandSpec{
					Suspend: true,
				},
				Status: map[string]*agentv1beta1.CommandStatus{
					testNodeIP: {
						Phase: agentv1beta1.CommandRunning,
					},
				},
			},
			expectComplete: false,
		},
		{
			name: "Complete - all success",
			cmd: &agentv1beta1.Command{
				Spec: agentv1beta1.CommandSpec{
					NodeSelector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							testNodeIP: testNodeIP,
						},
					},
				},
				Status: map[string]*agentv1beta1.CommandStatus{
					testNodeIP: {
						Phase:  agentv1beta1.CommandComplete,
						Status: metav1.ConditionTrue,
					},
				},
			},
			expectComplete: true,
		},
		{
			name: "Complete - with failure",
			cmd: &agentv1beta1.Command{
				Spec: agentv1beta1.CommandSpec{
					NodeSelector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							testNodeIP:  testNodeIP,
							testNodeIP2: testNodeIP2,
						},
					},
				},
				Status: map[string]*agentv1beta1.CommandStatus{
					testNodeIP: {
						Phase:  agentv1beta1.CommandComplete,
						Status: metav1.ConditionTrue,
					},
					testNodeIP2: {
						Phase:  agentv1beta1.CommandFailed,
						Status: metav1.ConditionFalse,
					},
				},
			},
			expectComplete: true,
		},
		{
			name: "Running command",
			cmd: &agentv1beta1.Command{
				Status: map[string]*agentv1beta1.CommandStatus{
					testNodeIP: {
						Phase: agentv1beta1.CommandRunning,
					},
				},
			},
			expectComplete: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			complete, _, _ := CheckCommandStatus(tt.cmd)
			assert.Equal(t, tt.expectComplete, complete)
		})
	}
}

func TestIsOwnerRefCommand(t *testing.T) {
	tests := []struct {
		name   string
		object *metav1.ObjectMeta
		cmd    *agentv1beta1.Command
		expect bool
	}{
		{
			name: "Is owner reference",
			object: &metav1.ObjectMeta{
				Name: "owner",
				UID:  "test-uid",
			},
			cmd: &agentv1beta1.Command{
				ObjectMeta: metav1.ObjectMeta{
					OwnerReferences: []metav1.OwnerReference{
						{
							Name: "owner",
							UID:  "test-uid",
						},
					},
				},
			},
			expect: true,
		},
		{
			name: "Not owner reference - different UID",
			object: &metav1.ObjectMeta{
				Name: "owner",
				UID:  "test-uid",
			},
			cmd: &agentv1beta1.Command{
				ObjectMeta: metav1.ObjectMeta{
					OwnerReferences: []metav1.OwnerReference{
						{
							Name: "owner",
							UID:  "different-uid",
						},
					},
				},
			},
			expect: false,
		},
		{
			name: "No owner references",
			object: &metav1.ObjectMeta{
				Name: "owner",
				UID:  "test-uid",
			},
			cmd: &agentv1beta1.Command{
				ObjectMeta: metav1.ObjectMeta{
					OwnerReferences: []metav1.OwnerReference{},
				},
			},
			expect: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsOwnerRefCommand(tt.object, *tt.cmd)
			assert.Equal(t, tt.expect, result)
		})
	}
}

func TestGetNodeSelector(t *testing.T) {
	nodes := bkenode.Nodes{
		{IP: testNodeIP, Hostname: testHost},
		{IP: testNodeIP2, Hostname: "node2"},
	}

	selector := getNodeSelector(nodes)

	assert.NotNil(t, selector)
	assert.NotNil(t, selector.MatchLabels)
	assert.Equal(t, testNodeIP, selector.MatchLabels[testNodeIP])
	assert.Equal(t, testNodeIP2, selector.MatchLabels[testNodeIP2])
}

func TestConstants(t *testing.T) {
	assert.Equal(t, "bootstrap-", BootstrapCommandNamePrefix)
	assert.Equal(t, "k8s-ha-deploy", HACommandName)
	assert.Equal(t, "k8s-env-init", K8sEnvCommandName)
	assert.Equal(t, "switch-cluster-", SwitchClusterCommandNamePrefix)
	assert.Equal(t, "reset-node-", ResetNodeCommandNamePrefix)
	assert.Equal(t, "upgrade-node-", UpgradeNodeCommandNamePrefix)
	assert.Equal(t, "ping-", PingCommandNamePrefix)
	assert.Equal(t, "collect-", CollectCertCommandNamePrefix)
	assert.Equal(t, "bke.bocloud.com/cluster-command", BKEClusterLabel)
	assert.Equal(t, "bke.bocloud.com/machine-command", BKEMachineLabel)
	assert.Equal(t, 3, DefaultBackoffLimit)
	assert.Equal(t, 1000, DefaultActiveDeadlineSecond)
	assert.Equal(t, 600, DefaultTTLSecondsAfterFinished)
	assert.Equal(t, cmdTimeout, DefaultWaitTimeout)
	assert.Equal(t, cmdInterval, DefaultWaitInterval)
}

func TestBootstrapConstants(t *testing.T) {
	assert.Equal(t, "bke.bocloud.com/master-init-command", MasterInitCommandLabel)
	assert.Equal(t, "bke.bocloud.com/master-join-command", MasterJoinCommandLabel)
	assert.Equal(t, "bke.bocloud.com/worker-join-command", WorkerJoinCommandLabel)
}

func TestTimeoutCaseResult(t *testing.T) {
	result := TimeoutCaseResult{
		Err:          nil,
		Complete:     true,
		SuccessNodes: []string{testNodeIP},
		FailedNodes:  []string{},
	}

	assert.True(t, result.Complete)
	assert.Nil(t, result.Err)
	assert.Equal(t, []string{testNodeIP}, result.SuccessNodes)
}

func TestCommandNodes(t *testing.T) {
	nodes := CommandNodes{
		SuccessNodes: []string{testNodeIP},
		FailedNodes:  []string{testNodeIP2},
	}

	assert.Equal(t, numC1, len(nodes.SuccessNodes))
	assert.Equal(t, numC1, len(nodes.FailedNodes))
}

func TestWaitCommandResult(t *testing.T) {
	result := WaitCommandResult{
		Err:          nil,
		Complete:     true,
		SuccessNodes: []string{testNodeIP},
		FailedNodes:  []string{},
	}

	assert.True(t, result.Complete)
	assert.Nil(t, result.Err)
}

func TestGenerateDefaultCommandSpecFields(t *testing.T) {
	spec := GenerateDefaultCommandSpec()

	assert.NotNil(t, spec)
	assert.Empty(t, spec.NodeName)
	assert.False(t, spec.Suspend)
	assert.Empty(t, spec.Commands)
	assert.Equal(t, DefaultBackoffLimit, spec.BackoffLimit)
	assert.Equal(t, DefaultActiveDeadlineSecond, spec.ActiveDeadlineSecond)
	assert.Equal(t, DefaultTTLSecondsAfterFinished, spec.TTLSecondsAfterFinished)
	assert.NotNil(t, spec.NodeSelector)
}

func TestBaseCommandValidate(t *testing.T) {
	tests := []struct {
		name    string
		cmd     *BaseCommand
		wantErr bool
	}{
		{
			name: "Valid base command",
			cmd: &BaseCommand{
				Client:    fake.NewClientBuilder().Build(),
				NameSpace: testNS,
				Scheme:    runtime.NewScheme(),
			},
			wantErr: false,
		},
		{
			name: "Nil client",
			cmd: &BaseCommand{
				Client:    nil,
				NameSpace: testNS,
				Scheme:    runtime.NewScheme(),
			},
			wantErr: true,
		},
		{
			name: "Nil scheme",
			cmd: &BaseCommand{
				Client:    fake.NewClientBuilder().Build(),
				NameSpace: testNS,
				Scheme:    nil,
			},
			wantErr: true,
		},
		{
			name: "Empty namespace",
			cmd: &BaseCommand{
				Client:    fake.NewClientBuilder().Build(),
				NameSpace: "",
				Scheme:    runtime.NewScheme(),
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.cmd.validate()
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestValidateBkeCommand(t *testing.T) {
	tests := []struct {
		name    string
		nodes   bkenode.Nodes
		config  string
		cmd     *BaseCommand
		wantErr bool
	}{
		{
			name:   "Valid command",
			nodes:  bkenode.Nodes{{IP: testNodeIP, Hostname: testHost}},
			config: testBKEConfig,
			cmd: &BaseCommand{
				Client:    fake.NewClientBuilder().Build(),
				NameSpace: testNS,
				Scheme:    runtime.NewScheme(),
			},
			wantErr: false,
		},
		{
			name:   "Empty nodes",
			nodes:  bkenode.Nodes{},
			config: testBKEConfig,
			cmd: &BaseCommand{
				Client:    fake.NewClientBuilder().Build(),
				NameSpace: testNS,
				Scheme:    runtime.NewScheme(),
			},
			wantErr: true,
		},
		{
			name:   "Empty config",
			nodes:  bkenode.Nodes{{IP: testNodeIP, Hostname: testHost}},
			config: "",
			cmd: &BaseCommand{
				Client:    fake.NewClientBuilder().Build(),
				NameSpace: testNS,
				Scheme:    runtime.NewScheme(),
			},
			wantErr: true,
		},
		{
			name:   "Nil client",
			nodes:  bkenode.Nodes{{IP: testNodeIP, Hostname: testHost}},
			config: testBKEConfig,
			cmd: &BaseCommand{
				Client:    nil,
				NameSpace: testNS,
				Scheme:    runtime.NewScheme(),
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidateBkeCommand(tt.nodes, tt.config, tt.cmd)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestBaseCommandSetCommandName(t *testing.T) {
	cmd := &BaseCommand{}
	cmd.setCommandName(testCmdName)
	assert.Equal(t, testCmdName, cmd.commandName)
}

func TestBaseCommandGetCommand(t *testing.T) {
	tests := []struct {
		name    string
		cmd     *BaseCommand
		wantErr bool
	}{
		{
			name: "Empty command name",
			cmd: &BaseCommand{
				Ctx:         context.Background(),
				Client:      fake.NewClientBuilder().Build(),
				NameSpace:   testNS,
				Scheme:      runtime.NewScheme(),
				commandName: "",
			},
			wantErr: true,
		},
		{
			name: "Command not found",
			cmd: &BaseCommand{
				Ctx:         context.Background(),
				Client:      fake.NewClientBuilder().Build(),
				NameSpace:   testNS,
				Scheme:      runtime.NewScheme(),
				commandName: "non-existent-command",
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			command, err := tt.cmd.GetCommand()
			if tt.wantErr {
				assert.Error(t, err)
				assert.Nil(t, command)
			}
		})
	}
}

func TestBaseCommandDeleteCommand(t *testing.T) {
	tests := []struct {
		name    string
		cmd     *BaseCommand
		wantErr bool
	}{
		{
			name: "Nil command",
			cmd: &BaseCommand{
				Ctx:       context.Background(),
				Client:    fake.NewClientBuilder().Build(),
				NameSpace: testNS,
				Scheme:    runtime.NewScheme(),
				Command:   nil,
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.cmd.deleteCommand()
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestBaseCommandWaitCommandComplete(t *testing.T) {
	cmd := &BaseCommand{
		Ctx:         context.Background(),
		Client:      fake.NewClientBuilder().Build(),
		NameSpace:   testNS,
		Scheme:      runtime.NewScheme(),
		commandName: "",
	}

	err, complete, nodes := cmd.waitCommandComplete()
	assert.Error(t, err)
	assert.False(t, complete)
	assert.Nil(t, nodes.SuccessNodes)
	assert.Nil(t, nodes.FailedNodes)
}

func TestBaseCommandWaitCommandCompleteWithStruct(t *testing.T) {
	cmd := &BaseCommand{
		Ctx:         context.Background(),
		Client:      fake.NewClientBuilder().Build(),
		NameSpace:   testNS,
		Scheme:      runtime.NewScheme(),
		commandName: "",
	}

	result := cmd.waitCommandCompleteWithStruct()
	assert.Error(t, result.Err)
	assert.False(t, result.Complete)
	assert.Nil(t, result.SuccessNodes)
	assert.Nil(t, result.FailedNodes)
}

func TestBaseCommandHandleTimeoutCase(t *testing.T) {
	cmd := &BaseCommand{
		Ctx:         context.Background(),
		Client:      fake.NewClientBuilder().Build(),
		NameSpace:   testNS,
		Scheme:      runtime.NewScheme(),
		commandName: "non-existent-command",
	}

	result := cmd.handleTimeoutCase(false, []string{}, []string{})
	assert.Error(t, result.Err)
	assert.False(t, result.Complete)
}

func TestClusterNameLabelSelectorRequirement(t *testing.T) {
	cmd := &BaseCommand{
		ClusterName: testClName,
	}

	requirement := cmd.ClusterNameLabelSelectorRequirement()
	assert.Equal(t, utils.ClusterNameLabelKey, requirement.Key)
	assert.Equal(t, metav1.LabelSelectorOpIn, requirement.Operator)
	assert.Contains(t, requirement.Values, testClName)
}

func TestBaseCommandBuildLabels(t *testing.T) {
	cmd := &BaseCommand{
		ClusterName: testClName,
	}

	labels := cmd.buildLabels("test-label", []string{"custom-label"})
	assert.NotNil(t, labels)
	assert.Contains(t, labels, "test-label")
	assert.Contains(t, labels, clusterv1.ClusterNameLabel)
	assert.Contains(t, labels, "custom-label")
}

func TestBaseCommandBuildLabelsWithoutClusterName(t *testing.T) {
	cmd := &BaseCommand{
		ClusterName: "",
	}

	labels := cmd.buildLabels("test-label", []string{})
	assert.NotNil(t, labels)
	assert.Contains(t, labels, "test-label")
	assert.NotContains(t, labels, clusterv1.ClusterNameLabel)
}

func TestBaseCommandSetOwnerReference(t *testing.T) {
	tests := []struct {
		name    string
		cmd     *BaseCommand
		wantErr bool
	}{
		{
			name: "Nil owner object",
			cmd: &BaseCommand{
				Scheme: runtime.NewScheme(),
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			command := &agentv1beta1.Command{}
			err := tt.cmd.setOwnerReference(command)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestBaseCommandHandleUniqueCommand(t *testing.T) {
	tests := []struct {
		name    string
		cmd     *BaseCommand
		wantErr bool
	}{
		{
			name: "Not unique command",
			cmd: &BaseCommand{
				Ctx:       context.Background(),
				Client:    fake.NewClientBuilder().Build(),
				NameSpace: testNS,
				Scheme:    runtime.NewScheme(),
				Unique:    false,
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.cmd.handleUniqueCommand("test-command")
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestBaseCommandWaitCommandCompleteWithDefaults(t *testing.T) {
	cmd := &BaseCommand{
		Ctx:          context.Background(),
		Client:       fake.NewClientBuilder().Build(),
		NameSpace:    testNS,
		Scheme:       runtime.NewScheme(),
		commandName:  "",
		WaitTimeout:  0,
		WaitInterval: 0,
	}

	result := cmd.waitCommandCompleteWithStruct()
	assert.Error(t, result.Err)
	assert.Equal(t, time.Duration(0), cmd.WaitInterval)
	assert.Equal(t, time.Duration(0), cmd.WaitTimeout)
}