package instance

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	v1 "sigs.k8s.io/karpenter/pkg/apis/v1"
)

// MockVPCClient mocks the VPC client interface
type MockVPCClient struct {
	mock.Mock
}

func (m *MockVPCClient) CreateInstance(ctx context.Context, instanceType string, userData string) (*Instance, error) {
	args := m.Called(ctx, instanceType, userData)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*Instance), args.Error(1)
}

func (m *MockVPCClient) DeleteInstance(ctx context.Context, instanceID string) error {
	args := m.Called(ctx, instanceID)
	return args.Error(0)
}

func TestProviderCreate(t *testing.T) {
	tests := []struct {
		name          string
		nodeClaim     *v1.NodeClaim
		setupMock     func(*MockVPCClient)
		expectedError bool
	}{
		{
			name: "successful instance creation",
			nodeClaim: &v1.NodeClaim{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-node",
				},
				Spec: v1.NodeClaimSpec{
					Requirements: []v1.NodeSelectorRequirementWithMinValues{
						{
							NodeSelectorRequirement: corev1.NodeSelectorRequirement{
								Key:      "node.kubernetes.io/instance-type",
								Operator: corev1.NodeSelectorOpIn,
								Values:   []string{"bx2-2x8"},
							},
						},
					},
				},
			},
			setupMock: func(m *MockVPCClient) {
				m.On("CreateInstance", mock.Anything, "bx2-2x8", mock.Anything).Return(&Instance{
					ID:         "test-instance-id",
					Name:       "test-instance",
					Status:     "running",
					PrivateIP:  "10.0.0.1",
					PublicIP:   "192.168.1.1",
					ProviderID: "ibm://test-instance-id",
				}, nil)
			},
			expectedError: false,
		},
		{
			name: "creation failure",
			nodeClaim: &v1.NodeClaim{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-node",
				},
				Spec: v1.NodeClaimSpec{
					Requirements: []v1.NodeSelectorRequirementWithMinValues{
						{
							NodeSelectorRequirement: corev1.NodeSelectorRequirement{
								Key:      "node.kubernetes.io/instance-type",
								Operator: corev1.NodeSelectorOpIn,
								Values:   []string{"bx2-2x8"},
							},
						},
					},
				},
			},
			setupMock: func(m *MockVPCClient) {
				m.On("CreateInstance", mock.Anything, "bx2-2x8", mock.Anything).Return(nil, assert.AnError)
			},
			expectedError: true,
		},
		{
			name: "missing instance type requirement",
			nodeClaim: &v1.NodeClaim{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-node",
				},
				Spec: v1.NodeClaimSpec{
					Requirements: []v1.NodeSelectorRequirementWithMinValues{},
				},
			},
			setupMock: func(m *MockVPCClient) {
				// No mock setup needed as it should fail before VPC client call
			},
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock VPC client
			mockVPCClient := &MockVPCClient{}
			tt.setupMock(mockVPCClient)

			// Create provider with mock client
			provider := &Provider{
				vpcClient: mockVPCClient,
			}

			// Test Create
			result, err := provider.Create(context.Background(), tt.nodeClaim)

			// Verify expectations
			if tt.expectedError {
				assert.Error(t, err)
				assert.Nil(t, result)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.NotEmpty(t, result.Status.ProviderID)
			}

			mockVPCClient.AssertExpectations(t)
		})
	}
}

func TestProviderDelete(t *testing.T) {
	tests := []struct {
		name          string
		nodeClaim     *v1.NodeClaim
		setupMock     func(*MockVPCClient)
		expectedError bool
	}{
		{
			name: "successful instance deletion",
			nodeClaim: &v1.NodeClaim{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-node",
				},
				Status: v1.NodeClaimStatus{
					ProviderID: "ibm://test-instance-id",
				},
			},
			setupMock: func(m *MockVPCClient) {
				m.On("DeleteInstance", mock.Anything, "test-instance-id").Return(nil)
			},
			expectedError: false,
		},
		{
			name: "deletion failure",
			nodeClaim: &v1.NodeClaim{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-node",
				},
				Status: v1.NodeClaimStatus{
					ProviderID: "ibm://test-instance-id",
				},
			},
			setupMock: func(m *MockVPCClient) {
				m.On("DeleteInstance", mock.Anything, "test-instance-id").Return(assert.AnError)
			},
			expectedError: true,
		},
		{
			name: "invalid provider ID format",
			nodeClaim: &v1.NodeClaim{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-node",
				},
				Status: v1.NodeClaimStatus{
					ProviderID: "invalid-provider-id",
				},
			},
			setupMock: func(m *MockVPCClient) {
				// No mock setup needed as it should fail before VPC client call
			},
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock VPC client
			mockVPCClient := &MockVPCClient{}
			tt.setupMock(mockVPCClient)

			// Create provider with mock client
			provider := &Provider{
				vpcClient: mockVPCClient,
			}

			// Test Delete
			err := provider.Delete(context.Background(), tt.nodeClaim)

			// Verify expectations
			if tt.expectedError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}

			mockVPCClient.AssertExpectations(t)
		})
	}
}

func TestExtractInstanceIDFromProviderID(t *testing.T) {
	tests := []struct {
		name          string
		providerID    string
		expectedID    string
		expectedError bool
	}{
		{
			name:          "valid provider ID",
			providerID:    "ibm://test-instance-id",
			expectedID:    "test-instance-id",
			expectedError: false,
		},
		{
			name:          "invalid provider ID format",
			providerID:    "invalid-provider-id",
			expectedID:    "",
			expectedError: true,
		},
		{
			name:          "empty provider ID",
			providerID:    "",
			expectedID:    "",
			expectedError: true,
		},
		{
			name:          "wrong prefix",
			providerID:    "aws://test-instance-id",
			expectedID:    "",
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			id, err := extractInstanceIDFromProviderID(tt.providerID)

			if tt.expectedError {
				assert.Error(t, err)
				assert.Empty(t, id)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expectedID, id)
			}
		})
	}
}
