Improved parameter validation

Change-Id: If2e7ec1b063a6e114a6c5582463af784b75c37b8
diff --git a/tools/schema_test.go b/tools/schema_test.go
new file mode 100644
index 0000000..4365537
--- /dev/null
+++ b/tools/schema_test.go
@@ -0,0 +1,362 @@
+package tools
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"testing"
+
+	"github.com/korap/korap-mcp/service"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestAllToolsSchemaCompliance ensures all tools have properly structured schemas
+func TestAllToolsSchemaCompliance(t *testing.T) {
+	client := &service.Client{}
+
+	tools := []Tool{
+		NewSearchTool(client),
+		NewMetadataTool(client),
+	}
+
+	for _, tool := range tools {
+		t.Run(tool.Name(), func(t *testing.T) {
+			schema := tool.InputSchema()
+
+			// Verify basic schema structure
+			validateBasicSchemaStructure(t, schema, tool.Name())
+
+			// Verify schema is valid JSON
+			validateSchemaIsValidJSON(t, schema, tool.Name())
+
+			// Verify schema documentation completeness
+			validateSchemaDocumentation(t, schema, tool.Name())
+		})
+	}
+}
+
+// validateBasicSchemaStructure checks that the schema has the required top-level properties
+func validateBasicSchemaStructure(t *testing.T, schema map[string]interface{}, toolName string) {
+	// Must be object type
+	assert.Equal(t, "object", schema["type"], "Tool %s schema must be object type", toolName)
+
+	// Must have properties
+	properties, exists := schema["properties"]
+	assert.True(t, exists, "Tool %s schema must have properties", toolName)
+	assert.IsType(t, map[string]interface{}{}, properties, "Tool %s properties must be an object", toolName)
+
+	// Must have required array
+	required, exists := schema["required"]
+	assert.True(t, exists, "Tool %s schema must have required array", toolName)
+	assert.IsType(t, []string{}, required, "Tool %s required must be string array", toolName)
+
+	// Should have title and description
+	assert.Contains(t, schema, "title", "Tool %s schema should have title", toolName)
+	assert.Contains(t, schema, "description", "Tool %s schema should have description", toolName)
+
+	// Should prevent additional properties for strict validation
+	assert.Contains(t, schema, "additionalProperties", "Tool %s schema should specify additionalProperties", toolName)
+	assert.Equal(t, false, schema["additionalProperties"], "Tool %s should not allow additional properties", toolName)
+}
+
+// validateSchemaIsValidJSON ensures the schema can be properly serialized to JSON
+func validateSchemaIsValidJSON(t *testing.T, schema map[string]interface{}, toolName string) {
+	jsonBytes, err := json.Marshal(schema)
+	require.NoError(t, err, "Tool %s schema must be serializable to JSON", toolName)
+
+	var unmarshalled map[string]interface{}
+	err = json.Unmarshal(jsonBytes, &unmarshalled)
+	require.NoError(t, err, "Tool %s schema JSON must be valid", toolName)
+
+	// Verify the unmarshalled schema has the same structure
+	assert.Equal(t, schema["type"], unmarshalled["type"], "Tool %s schema type must survive JSON round-trip", toolName)
+}
+
+// validateSchemaDocumentation checks that all properties have proper documentation
+func validateSchemaDocumentation(t *testing.T, schema map[string]interface{}, toolName string) {
+	properties, ok := schema["properties"].(map[string]interface{})
+	require.True(t, ok, "Tool %s properties must be accessible", toolName)
+
+	for propName, propSchema := range properties {
+		propMap, ok := propSchema.(map[string]interface{})
+		require.True(t, ok, "Tool %s property %s must be an object", toolName, propName)
+
+		// Must have type
+		assert.Contains(t, propMap, "type", "Tool %s property %s must have type", toolName, propName)
+
+		// Must have description
+		assert.Contains(t, propMap, "description", "Tool %s property %s must have description", toolName, propName)
+
+		description, ok := propMap["description"].(string)
+		assert.True(t, ok, "Tool %s property %s description must be string", toolName, propName)
+		assert.NotEmpty(t, description, "Tool %s property %s description must not be empty", toolName, propName)
+		assert.Greater(t, len(description), 10, "Tool %s property %s description should be descriptive", toolName, propName)
+
+		// String properties should have examples
+		if propType, ok := propMap["type"].(string); ok && propType == "string" {
+			if enumValues, hasEnum := propMap["enum"]; hasEnum {
+				// Enum properties should have examples matching enum values
+				assert.Contains(t, propMap, "examples", "Tool %s property %s with enum should have examples", toolName, propName)
+				examples, ok := propMap["examples"].([]string)
+				assert.True(t, ok, "Tool %s property %s examples must be string array", toolName, propName)
+
+				enumArray, ok := enumValues.([]string)
+				assert.True(t, ok, "Tool %s property %s enum must be string array", toolName, propName)
+
+				// All examples should be valid enum values
+				for _, example := range examples {
+					assert.Contains(t, enumArray, example, "Tool %s property %s example '%s' must be valid enum value", toolName, propName, example)
+				}
+			} else {
+				// Regular string properties should have examples
+				assert.Contains(t, propMap, "examples", "Tool %s property %s should have examples", toolName, propName)
+			}
+		}
+
+		// Integer properties should have examples and constraints
+		if propType, ok := propMap["type"].(string); ok && propType == "integer" {
+			assert.Contains(t, propMap, "examples", "Tool %s property %s should have examples", toolName, propName)
+
+			// Should have minimum/maximum constraints
+			assert.Contains(t, propMap, "minimum", "Tool %s property %s should have minimum", toolName, propName)
+			assert.Contains(t, propMap, "maximum", "Tool %s property %s should have maximum", toolName, propName)
+		}
+	}
+}
+
+// TestSchemaExamples verifies that schema examples are realistic and valid
+func TestSchemaExamples(t *testing.T) {
+	client := &service.Client{}
+
+	t.Run("SearchTool", func(t *testing.T) {
+		tool := NewSearchTool(client)
+		schema := tool.InputSchema()
+
+		properties := schema["properties"].(map[string]interface{})
+
+		// Test query examples
+		queryProp := properties["query"].(map[string]interface{})
+		examples := queryProp["examples"].([]string)
+
+		for _, example := range examples {
+			assert.NotEmpty(t, example, "Query example should not be empty")
+			assert.LessOrEqual(t, len(example), 100, "Query example should be reasonably short")
+		}
+
+		// Test corpus examples
+		corpusProp := properties["corpus"].(map[string]interface{})
+		corpusExamples := corpusProp["examples"].([]string)
+
+		for _, example := range corpusExamples {
+			assert.NotEmpty(t, example, "Corpus example should not be empty")
+			// Should match the pattern defined in the schema
+			pattern := corpusProp["pattern"].(string)
+			assert.NotEmpty(t, pattern, "Corpus should have validation pattern")
+		}
+	})
+
+	t.Run("MetadataTool", func(t *testing.T) {
+		tool := NewMetadataTool(client)
+		schema := tool.InputSchema()
+
+		properties := schema["properties"].(map[string]interface{})
+
+		// Test action examples
+		actionProp := properties["action"].(map[string]interface{})
+		examples := actionProp["examples"].([]string)
+		enumValues := actionProp["enum"].([]string)
+
+		for _, example := range examples {
+			assert.Contains(t, enumValues, example, "Action example should be valid enum value")
+		}
+	})
+}
+
+// TestSchemaConstraints verifies that schema constraints are reasonable
+func TestSchemaConstraints(t *testing.T) {
+	client := &service.Client{}
+
+	t.Run("SearchTool", func(t *testing.T) {
+		tool := NewSearchTool(client)
+		schema := tool.InputSchema()
+		properties := schema["properties"].(map[string]interface{})
+
+		// Test query constraints
+		queryProp := properties["query"].(map[string]interface{})
+		minLength := queryProp["minLength"].(int)
+		maxLength := queryProp["maxLength"].(int)
+
+		assert.Greater(t, minLength, 0, "Query minimum length should be positive")
+		assert.Greater(t, maxLength, minLength, "Query maximum length should be greater than minimum")
+		assert.LessOrEqual(t, maxLength, 10000, "Query maximum length should be reasonable")
+
+		// Test count constraints
+		countProp := properties["count"].(map[string]interface{})
+		minimum := countProp["minimum"].(int)
+		maximum := countProp["maximum"].(int)
+		defaultValue := countProp["default"].(int)
+
+		assert.GreaterOrEqual(t, minimum, 0, "Count minimum should be non-negative")
+		assert.Greater(t, maximum, minimum, "Count maximum should be greater than minimum")
+		assert.GreaterOrEqual(t, defaultValue, minimum, "Count default should be >= minimum")
+		assert.LessOrEqual(t, defaultValue, maximum, "Count default should be <= maximum")
+	})
+}
+
+// TestSchemaVersioning ensures schema structure is backwards compatible
+func TestSchemaVersioning(t *testing.T) {
+	client := &service.Client{}
+
+	// This test would be extended when we add new versions
+	// For now, we ensure the basic structure is stable
+
+	tools := []Tool{
+		NewSearchTool(client),
+		NewMetadataTool(client),
+	}
+
+	for _, tool := range tools {
+		t.Run(tool.Name(), func(t *testing.T) {
+			schema := tool.InputSchema()
+
+			// Core schema structure must remain stable
+			assert.Equal(t, "object", schema["type"])
+			assert.Contains(t, schema, "properties")
+			assert.Contains(t, schema, "required")
+
+			// Tool-specific stability checks
+			switch tool.Name() {
+			case "korap_search":
+				properties := schema["properties"].(map[string]interface{})
+				assert.Contains(t, properties, "query", "Search tool must always have query parameter")
+
+				required := schema["required"].([]string)
+				assert.Contains(t, required, "query", "Search tool must always require query parameter")
+
+			case "korap_metadata":
+				properties := schema["properties"].(map[string]interface{})
+				assert.Contains(t, properties, "action", "Metadata tool must always have action parameter")
+
+				required := schema["required"].([]string)
+				assert.Contains(t, required, "action", "Metadata tool must always require action parameter")
+			}
+		})
+	}
+}
+
+// BenchmarkSchemaGeneration measures schema generation performance
+func BenchmarkSchemaGeneration(b *testing.B) {
+	client := &service.Client{}
+
+	tools := []Tool{
+		NewSearchTool(client),
+		NewMetadataTool(client),
+	}
+
+	for _, tool := range tools {
+		b.Run(tool.Name(), func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				schema := tool.InputSchema()
+				_ = schema // Prevent optimization
+			}
+		})
+	}
+}
+
+// TestSchemaDeepCopy ensures schemas are independent instances
+func TestSchemaDeepCopy(t *testing.T) {
+	client := &service.Client{}
+	tool := NewSearchTool(client)
+
+	// Get two schema instances
+	schema1 := tool.InputSchema()
+	schema2 := tool.InputSchema()
+
+	// They should be equal in content but different instances
+	assert.True(t, reflect.DeepEqual(schema1, schema2), "Schemas should have equal content")
+
+	// Modifying one should not affect the other
+	schema1["modified"] = true
+	assert.NotContains(t, schema2, "modified", "Schema instances should be independent")
+}
+
+// TestSchemaHelpGeneration verifies schema can be used to generate help text
+func TestSchemaHelpGeneration(t *testing.T) {
+	client := &service.Client{}
+
+	tools := []Tool{
+		NewSearchTool(client),
+		NewMetadataTool(client),
+	}
+
+	for _, tool := range tools {
+		t.Run(tool.Name(), func(t *testing.T) {
+			schema := tool.InputSchema()
+
+			// Generate help text from schema
+			helpText := generateHelpText(schema, tool.Name())
+
+			// Verify help text contains essential information
+			assert.Contains(t, helpText, tool.Name(), "Help text should contain tool name")
+			assert.Contains(t, helpText, "Parameters:", "Help text should list parameters")
+
+			// Should contain information about required parameters
+			required := schema["required"].([]string)
+			for _, param := range required {
+				assert.Contains(t, helpText, param, "Help text should mention required parameter %s", param)
+				assert.Contains(t, helpText, "(required)", "Help text should mark required parameters")
+			}
+		})
+	}
+}
+
+// generateHelpText creates human-readable help text from a schema
+func generateHelpText(schema map[string]interface{}, toolName string) string {
+	help := fmt.Sprintf("Tool: %s\n", toolName)
+
+	if desc, ok := schema["description"].(string); ok {
+		help += fmt.Sprintf("Description: %s\n\n", desc)
+	}
+
+	properties, ok := schema["properties"].(map[string]interface{})
+	if !ok {
+		return help
+	}
+
+	required, _ := schema["required"].([]string)
+	requiredMap := make(map[string]bool)
+	for _, req := range required {
+		requiredMap[req] = true
+	}
+
+	help += "Parameters:\n"
+	for paramName, paramSchema := range properties {
+		paramMap, ok := paramSchema.(map[string]interface{})
+		if !ok {
+			continue
+		}
+
+		requiredText := ""
+		if requiredMap[paramName] {
+			requiredText = " (required)"
+		}
+
+		paramType, _ := paramMap["type"].(string)
+		description, _ := paramMap["description"].(string)
+
+		help += fmt.Sprintf("  %s (%s)%s: %s\n", paramName, paramType, requiredText, description)
+
+		// Add enum values if present
+		if enum, ok := paramMap["enum"].([]string); ok {
+			help += fmt.Sprintf("    Allowed values: %v\n", enum)
+		}
+
+		// Add examples if present
+		if examples, ok := paramMap["examples"]; ok {
+			help += fmt.Sprintf("    Examples: %v\n", examples)
+		}
+	}
+
+	return help
+}