package action

import (
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	dataplanemgrmocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
)

func TestPreCreateSharedPoolAction_Name(t *testing.T) {
	dataplaneManager := dataplanemocks.NewManager(t)
	dataplaneClient := dataplanemgrmocks.NewClient(t)
	action := NewPreCreateSharedPoolAction(dataplaneManager, dataplaneClient)

	expected := PreCreateSharedPoolActionName
	assert.Equal(t, expected, action.Name())
}

func TestPreCreateSharedPoolAction_Invoke(t *testing.T) {
	tests := []struct {
		name        string
		dataplaneId string
		setupMocks  func(*dataplanemocks.Manager, *dataplanemgrmocks.Client)
		expectError bool
		errorMsg    string
	}{
		{
			name:        "successful invoke with active dataplane",
			dataplaneId: "dp-active-123",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				// Mock successful GetDataPlane call with Active status
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp-active-123"
				})).Return(&dplanemgrv1.DataPlane{
					Status: dplanemgrv1.DataPlane_Active,
				}, nil)
			},
			expectError: false,
		},
		{
			name:        "dataplane inactive should fail",
			dataplaneId: "dp-inactive-456",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp-inactive-456"
				})).Return(&dplanemgrv1.DataPlane{
					Status: dplanemgrv1.DataPlane_Creating,
				}, nil)
			},
			expectError: true,
			errorMsg:    "dataplane dp-inactive-456 is not ready, current status: Inactive",
		},
		{
			name:        "dataplane creating should fail",
			dataplaneId: "dp-creating-789",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp-creating-789"
				})).Return(&dplanemgrv1.DataPlane{
					Status: dplanemgrv1.DataPlane_Creating,
				}, nil)
			},
			expectError: true,
			errorMsg:    "dataplane dp-creating-789 is not ready, current status: Creating",
		},
		{
			name:        "dataplane deleting should fail",
			dataplaneId: "dp-deleting-101",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp-deleting-101"
				})).Return(&dplanemgrv1.DataPlane{
					Status: dplanemgrv1.DataPlane_Deleting,
				}, nil)
			},
			expectError: true,
			errorMsg:    "dataplane dp-deleting-101 is not ready, current status: Deleting",
		},
		{
			name:        "dataplane not found",
			dataplaneId: "dp-not-found-404",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp-not-found-404"
				})).Return(nil, errors.New("dataplane not found"))
			},
			expectError: true,
			errorMsg:    "failed to get dataplane: dataplane not found",
		},
		{
			name:        "missing dataplane id",
			dataplaneId: "",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				// No mock setup needed for this case
			},
			expectError: true,
			errorMsg:    "failed to get dataplane id",
		},
		{
			name:        "grpc error from client",
			dataplaneId: "dp-grpc-error",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp-grpc-error"
				})).Return(nil, errors.New("grpc: connection refused"))
			},
			expectError: true,
			errorMsg:    "failed to get dataplane: grpc: connection refused",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mocks
			dataplaneManager := dataplanemocks.NewManager(t)
			dataplaneClient := dataplanemgrmocks.NewClient(t)

			// Setup mocks
			tt.setupMocks(dataplaneManager, dataplaneClient)

			// Create action
			action := NewPreCreateSharedPoolAction(dataplaneManager, dataplaneClient)

			// Prepare arguments
			args := actions.ActionArgs{}
			if tt.dataplaneId != "" {
				args[workflow.DataplaneIdKey] = actions.ActionArgString(tt.dataplaneId)
			}

			// Execute test
			err := action.Invoke(args)

			// Validate results
			if tt.expectError {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestPreCreateSharedPoolAction_Invoke_InvalidArgs(t *testing.T) {
	tests := []struct {
		name        string
		args        actions.ActionArgs
		setupMocks  func(*dataplanemocks.Manager, *dataplanemgrmocks.Client)
		expectError bool
		errorMsg    string
	}{
		{
			name:        "missing dataplane id key",
			args:        actions.ActionArgs{},
			setupMocks:  func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {},
			expectError: true,
			errorMsg:    "failed to get dataplane id",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mocks
			dataplaneManager := dataplanemocks.NewManager(t)
			dataplaneClient := dataplanemgrmocks.NewClient(t)

			// Setup mocks
			tt.setupMocks(dataplaneManager, dataplaneClient)

			// Create action
			action := NewPreCreateSharedPoolAction(dataplaneManager, dataplaneClient)

			// Execute test
			err := action.Invoke(tt.args)

			// Validate results
			if tt.expectError {
				require.Error(t, err)
				if tt.errorMsg != "" {
					assert.Contains(t, err.Error(), tt.errorMsg)
				}
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestPreCreateSharedPoolAction_Invoke_EdgeCases(t *testing.T) {
	tests := []struct {
		name        string
		dataplaneId string
		setupMocks  func(*dataplanemocks.Manager, *dataplanemgrmocks.Client)
		expectError bool
		errorMsg    string
	}{
		{
			name:        "very long dataplane id",
			dataplaneId: "very-long-dataplane-id-that-exceeds-normal-length-limits-but-should-still-work-fine",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "very-long-dataplane-id-that-exceeds-normal-length-limits-but-should-still-work-fine"
				})).Return(&dplanemgrv1.DataPlane{
					Status: dplanemgrv1.DataPlane_Active,
				}, nil)
			},
			expectError: false,
		},
		{
			name:        "special characters in dataplane id",
			dataplaneId: "dp@123#456$789",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp@123#456$789"
				})).Return(&dplanemgrv1.DataPlane{
					Status: dplanemgrv1.DataPlane_Active,
				}, nil)
			},
			expectError: false,
		},
		{
			name:        "whitespace in dataplane id",
			dataplaneId: "  dp-123  ",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "  dp-123  "
				})).Return(&dplanemgrv1.DataPlane{
					Status: dplanemgrv1.DataPlane_Active,
				}, nil)
			},
			expectError: false,
		},
		{
			name:        "dataplane with unknown status",
			dataplaneId: "dp-unknown-status",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp-unknown-status"
				})).Return(&dplanemgrv1.DataPlane{
					Status: dplanemgrv1.DataPlane_Status(999), // Unknown status
				}, nil)
			},
			expectError: true,
			errorMsg:    "dataplane dp-unknown-status is not ready",
		},
		{
			name:        "dataplane with empty response",
			dataplaneId: "dp-empty-response",
			setupMocks: func(manager *dataplanemocks.Manager, client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.DataPlaneId == "dp-empty-response"
				})).Return(&dplanemgrv1.DataPlane{}, nil)
			},
			expectError: true,
			errorMsg:    "dataplane dp-empty-response is not ready",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mocks
			dataplaneManager := dataplanemocks.NewManager(t)
			dataplaneClient := dataplanemgrmocks.NewClient(t)

			// Setup mocks
			tt.setupMocks(dataplaneManager, dataplaneClient)

			// Create action
			action := NewPreCreateSharedPoolAction(dataplaneManager, dataplaneClient)

			// Prepare arguments
			args := actions.ActionArgs{
				workflow.DataplaneIdKey: actions.ActionArgString(tt.dataplaneId),
			}

			// Execute test
			err := action.Invoke(args)

			// Validate results
			if tt.expectError {
				require.Error(t, err)
				if tt.errorMsg != "" {
					assert.Contains(t, err.Error(), tt.errorMsg)
				}
			} else {
				require.NoError(t, err)
			}
		})
	}
}

// TestPreCreateSharedPoolAction_Invoke_Comprehensive tests comprehensive scenarios with detailed validation
func TestPreCreateSharedPoolAction_Invoke_Comprehensive(t *testing.T) {
	// Test action name consistency
	t.Run("action name consistency", func(t *testing.T) {
		dataplaneManager := dataplanemocks.NewManager(t)
		dataplaneClient := dataplanemgrmocks.NewClient(t)
		action := NewPreCreateSharedPoolAction(dataplaneManager, dataplaneClient)
		assert.Equal(t, PreCreateSharedPoolActionName, action.Name())
		assert.Equal(t, "pre-create-shared-pool", action.Name())
	})

	// Test successful execution with mock validation
	t.Run("successful execution validates mock calls", func(t *testing.T) {
		dataplaneManager := dataplanemocks.NewManager(t)
		dataplaneClient := dataplanemgrmocks.NewClient(t)

		// Setup specific mock expectations
		dataplaneClient.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
			return req.DataPlaneId == "test-dataplane-123"
		})).Return(&dplanemgrv1.DataPlane{
			Status: dplanemgrv1.DataPlane_Active,
		}, nil).Once() // Expect exactly one call

		action := NewPreCreateSharedPoolAction(dataplaneManager, dataplaneClient)

		args := actions.ActionArgs{
			workflow.DataplaneIdKey: actions.ActionArgString("test-dataplane-123"),
		}

		err := action.Invoke(args)
		require.NoError(t, err)

		// Verify all mock expectations were met (automatically done by testify mocks on cleanup)
	})

	// Test error propagation
	t.Run("error propagation maintains context", func(t *testing.T) {
		dataplaneManager := dataplanemocks.NewManager(t)
		dataplaneClient := dataplanemgrmocks.NewClient(t)

		originalError := errors.New("original grpc error")
		dataplaneClient.On("GetDataPlane", mock.Anything, mock.Anything).Return(nil, originalError)

		action := NewPreCreateSharedPoolAction(dataplaneManager, dataplaneClient)

		args := actions.ActionArgs{
			workflow.DataplaneIdKey: actions.ActionArgString("error-dataplane"),
		}

		err := action.Invoke(args)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "failed to get dataplane")
		assert.Contains(t, err.Error(), "original grpc error")
	})
}
