package mcp

import (
	"testing"

	"github.com/containers/kubernetes-mcp-server/pkg/api"
	"github.com/google/jsonschema-go/jsonschema"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"k8s.io/utils/ptr"
)

// createTestTool creates a basic ServerTool for testing
func createTestTool(name string) api.ServerTool {
	return api.ServerTool{
		Tool: api.Tool{
			Name:        name,
			Description: "A test tool",
			InputSchema: &jsonschema.Schema{
				Type:       "object",
				Properties: make(map[string]*jsonschema.Schema),
			},
		},
	}
}

// createTestToolWithNilSchema creates a ServerTool with nil InputSchema for testing
func createTestToolWithNilSchema(name string) api.ServerTool {
	return api.ServerTool{
		Tool: api.Tool{
			Name:        name,
			Description: "A test tool",
			InputSchema: nil,
		},
	}
}

// createTestToolWithNilProperties creates a ServerTool with nil Properties for testing
func createTestToolWithNilProperties(name string) api.ServerTool {
	return api.ServerTool{
		Tool: api.Tool{
			Name:        name,
			Description: "A test tool",
			InputSchema: &jsonschema.Schema{
				Type:       "object",
				Properties: nil,
			},
		},
	}
}

// createTestToolWithExistingProperties creates a ServerTool with existing properties for testing
func createTestToolWithExistingProperties(name string) api.ServerTool {
	return api.ServerTool{
		Tool: api.Tool{
			Name:        name,
			Description: "A test tool",
			InputSchema: &jsonschema.Schema{
				Type: "object",
				Properties: map[string]*jsonschema.Schema{
					"existing-prop": {Type: "string"},
				},
			},
		},
	}
}

func TestWithClusterParameter(t *testing.T) {
	tests := []struct {
		name                string
		defaultCluster      string
		targetParameterName string
		clusters            []string
		toolName            string
		toolFactory         func(string) api.ServerTool
		expectCluster       bool
		expectEnum          bool
		enumCount           int
	}{
		{
			name:           "adds cluster parameter when multiple clusters provided",
			defaultCluster: "default-cluster",
			clusters:       []string{"cluster1", "cluster2", "cluster3"},
			toolName:       "test-tool",
			toolFactory:    createTestTool,
			expectCluster:  true,
			expectEnum:     true,
			enumCount:      3,
		},
		{
			name:           "does not add cluster parameter when single cluster provided",
			defaultCluster: "default-cluster",
			clusters:       []string{"single-cluster"},
			toolName:       "test-tool",
			toolFactory:    createTestTool,
			expectCluster:  false,
			expectEnum:     false,
			enumCount:      0,
		},
		{
			name:           "creates InputSchema when nil",
			defaultCluster: "default-cluster",
			clusters:       []string{"cluster1", "cluster2"},
			toolName:       "test-tool",
			toolFactory:    createTestToolWithNilSchema,
			expectCluster:  true,
			expectEnum:     true,
			enumCount:      2,
		},
		{
			name:           "creates Properties map when nil",
			defaultCluster: "default-cluster",
			clusters:       []string{"cluster1", "cluster2"},
			toolName:       "test-tool",
			toolFactory:    createTestToolWithNilProperties,
			expectCluster:  true,
			expectEnum:     true,
			enumCount:      2,
		},
		{
			name:           "preserves existing properties",
			defaultCluster: "default-cluster",
			clusters:       []string{"cluster1", "cluster2"},
			toolName:       "test-tool",
			toolFactory:    createTestToolWithExistingProperties,
			expectCluster:  true,
			expectEnum:     true,
			enumCount:      2,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.targetParameterName == "" {
				tt.targetParameterName = "cluster"
			}
			mutator := WithTargetParameter(tt.defaultCluster, tt.targetParameterName, tt.clusters)
			tool := tt.toolFactory(tt.toolName)
			originalTool := tool // Keep reference to check if tool was unchanged

			result := mutator(tool)

			if !tt.expectCluster {
				if tt.toolName == "skip-this-tool" {
					// For skipped tools, the entire tool should be unchanged
					assert.Equal(t, originalTool, result)
				} else {
					// For single cluster, schema should exist but no cluster property
					require.NotNil(t, result.Tool.InputSchema)
					require.NotNil(t, result.Tool.InputSchema.Properties)
					_, exists := result.Tool.InputSchema.Properties["cluster"]
					assert.False(t, exists, "cluster property should not exist")
				}
				return
			}

			// Common assertions for cases where cluster parameter should be added
			require.NotNil(t, result.Tool.InputSchema)
			assert.Equal(t, "object", result.Tool.InputSchema.Type)
			require.NotNil(t, result.Tool.InputSchema.Properties)

			clusterProperty, exists := result.Tool.InputSchema.Properties["cluster"]
			assert.True(t, exists, "cluster property should exist")
			assert.NotNil(t, clusterProperty)
			assert.Equal(t, "string", clusterProperty.Type)
			assert.Contains(t, clusterProperty.Description, tt.defaultCluster)

			if tt.expectEnum {
				assert.NotNil(t, clusterProperty.Enum)
				assert.Equal(t, tt.enumCount, len(clusterProperty.Enum))
				for _, cluster := range tt.clusters {
					assert.Contains(t, clusterProperty.Enum, cluster)
				}
			}
		})
	}
}

func TestCreateClusterProperty(t *testing.T) {
	tests := []struct {
		name           string
		defaultCluster string
		targetName     string
		clusters       []string
		expectEnum     bool
		expectedCount  int
	}{
		{
			name:           "creates property with enum when clusters <= maxClustersInEnum",
			defaultCluster: "default",
			targetName:     "cluster",
			clusters:       []string{"cluster1", "cluster2", "cluster3"},
			expectEnum:     true,
			expectedCount:  3,
		},
		{
			name:           "creates property without enum when clusters > maxClustersInEnum",
			defaultCluster: "default",
			targetName:     "cluster",
			clusters:       make([]string, maxTargetsInEnum+5), // 20 clusters
			expectEnum:     false,
			expectedCount:  0,
		},
		{
			name:           "creates property with exact maxClustersInEnum clusters",
			defaultCluster: "default",
			targetName:     "cluster",
			clusters:       make([]string, maxTargetsInEnum),
			expectEnum:     true,
			expectedCount:  maxTargetsInEnum,
		},
		{
			name:           "handles single cluster",
			defaultCluster: "default",
			targetName:     "cluster",
			clusters:       []string{"single-cluster"},
			expectEnum:     true,
			expectedCount:  1,
		},
		{
			name:           "handles empty clusters list",
			defaultCluster: "default",
			targetName:     "cluster",
			clusters:       []string{},
			expectEnum:     true,
			expectedCount:  0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Initialize clusters with names if they were created with make()
			if len(tt.clusters) > 3 && tt.clusters[0] == "" {
				for i := range tt.clusters {
					tt.clusters[i] = "cluster" + string(rune('A'+i))
				}
			}

			property := createTargetProperty(tt.defaultCluster, tt.targetName, tt.clusters)

			assert.Equal(t, "string", property.Type)
			assert.Contains(t, property.Description, tt.defaultCluster)
			assert.Contains(t, property.Description, "Defaults to "+tt.defaultCluster+" if not set")

			if tt.expectEnum {
				assert.NotNil(t, property.Enum, "enum should be created")
				assert.Equal(t, tt.expectedCount, len(property.Enum))
				if tt.expectedCount > 0 && tt.expectedCount <= 3 {
					// Only check specific values for small, predefined lists
					for _, cluster := range tt.clusters {
						assert.Contains(t, property.Enum, cluster)
					}
				}
			} else {
				assert.Nil(t, property.Enum, "enum should not be created for too many clusters")
			}
		})
	}
}

func TestToolMutatorType(t *testing.T) {
	t.Run("ToolMutator type can be used as function", func(t *testing.T) {
		var mutator ToolMutator = func(tool api.ServerTool) api.ServerTool {
			tool.Tool.Name = "modified-" + tool.Tool.Name
			return tool
		}

		originalTool := createTestTool("original")
		result := mutator(originalTool)
		assert.Equal(t, "modified-original", result.Tool.Name)
	})
}

func TestMaxClustersInEnumConstant(t *testing.T) {
	t.Run("maxClustersInEnum has expected value", func(t *testing.T) {
		assert.Equal(t, 5, maxTargetsInEnum, "maxClustersInEnum should be 5")
	})
}

type TargetParameterToolMutatorSuite struct {
	suite.Suite
}

func (s *TargetParameterToolMutatorSuite) TestClusterAwareTool() {
	tm := WithTargetParameter("default-cluster", "cluster", []string{"cluster-1", "cluster-2", "cluster-3"})
	tool := createTestTool("cluster-aware-tool")
	// Tools are cluster-aware by default
	tm(tool)
	s.Require().NotNil(tool.Tool.InputSchema.Properties)
	s.Run("adds cluster parameter", func() {
		s.NotNil(tool.Tool.InputSchema.Properties["cluster"], "Expected cluster property to be added")
	})
	s.Run("adds correct description", func() {
		desc := tool.Tool.InputSchema.Properties["cluster"].Description
		s.Contains(desc, "Optional parameter selecting which cluster to run the tool in", "Expected description to mention cluster selection")
		s.Contains(desc, "Defaults to default-cluster if not set", "Expected description to mention default cluster")
	})
	s.Run("adds enum with clusters", func() {
		s.Require().NotNil(tool.Tool.InputSchema.Properties["cluster"])
		enum := tool.Tool.InputSchema.Properties["cluster"].Enum
		s.NotNilf(enum, "Expected enum to be set")
		s.Equal(3, len(enum), "Expected enum to have 3 entries")
		s.Contains(enum, "cluster-1", "Expected enum to contain cluster-1")
		s.Contains(enum, "cluster-2", "Expected enum to contain cluster-2")
		s.Contains(enum, "cluster-3", "Expected enum to contain cluster-3")
	})
}

func (s *TargetParameterToolMutatorSuite) TestClusterAwareToolSingleCluster() {
	tm := WithTargetParameter("default", "cluster", []string{"only-cluster"})
	tool := createTestTool("cluster-aware-tool-single-cluster")
	// Tools are cluster-aware by default
	tm(tool)
	s.Run("does not add cluster parameter for single cluster", func() {
		s.Nilf(tool.Tool.InputSchema.Properties["cluster"], "Expected cluster property to not be added for single cluster")
	})
}

func (s *TargetParameterToolMutatorSuite) TestClusterAwareToolMultipleClusters() {
	tm := WithTargetParameter("default", "cluster", []string{"cluster-1", "cluster-2", "cluster-3", "cluster-4", "cluster-5", "cluster-6"})
	tool := createTestTool("cluster-aware-tool-multiple-clusters")
	// Tools are cluster-aware by default
	tm(tool)
	s.Run("adds cluster parameter", func() {
		s.NotNilf(tool.Tool.InputSchema.Properties["cluster"], "Expected cluster property to be added")
	})
	s.Run("does not add enum when list of clusters is > 5", func() {
		s.Require().NotNil(tool.Tool.InputSchema.Properties["cluster"])
		enum := tool.Tool.InputSchema.Properties["cluster"].Enum
		s.Nilf(enum, "Expected enum to not be set for too many clusters")
	})
}

func (s *TargetParameterToolMutatorSuite) TestNonClusterAwareTool() {
	tm := WithTargetParameter("default", "cluster", []string{"cluster-1", "cluster-2"})
	tool := createTestTool("non-cluster-aware-tool")
	tool.ClusterAware = ptr.To(false)
	tm(tool)
	s.Run("does not add cluster parameter", func() {
		s.Nilf(tool.Tool.InputSchema.Properties["cluster"], "Expected cluster property to not be added")
	})
}

func TestTargetParameterToolMutator(t *testing.T) {
	suite.Run(t, new(TargetParameterToolMutatorSuite))
}
