package toolsets

import (
	"context"
	"testing"

	gobuildkite "github.com/buildkite/go-buildkite/v4"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestToolDefinition_IsReadOnly(t *testing.T) {
	tests := []struct {
		name     string
		tool     ToolDefinition
		expected bool
	}{
		{
			name: "read-only tool with hint set to true",
			tool: ToolDefinition{
				Tool: mcp.Tool{
					Annotations: mcp.ToolAnnotation{
						ReadOnlyHint: func() *bool { b := true; return &b }(),
					},
				},
			},
			expected: true,
		},
		{
			name: "read-write tool with hint set to false",
			tool: ToolDefinition{
				Tool: mcp.Tool{
					Annotations: mcp.ToolAnnotation{
						ReadOnlyHint: func() *bool { b := false; return &b }(),
					},
				},
			},
			expected: false,
		},
		{
			name: "tool with no read-only hint",
			tool: ToolDefinition{
				Tool: mcp.Tool{
					Annotations: mcp.ToolAnnotation{
						ReadOnlyHint: nil,
					},
				},
			},
			expected: false,
		},
	}

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

func TestToolset_GetReadOnlyTools(t *testing.T) {
	readOnlyTool := ToolDefinition{
		Tool: mcp.Tool{
			Name: "read-only-tool",
			Annotations: mcp.ToolAnnotation{
				ReadOnlyHint: func() *bool { b := true; return &b }(),
			},
		},
	}

	readWriteTool := ToolDefinition{
		Tool: mcp.Tool{
			Name: "read-write-tool",
			Annotations: mcp.ToolAnnotation{
				ReadOnlyHint: func() *bool { b := false; return &b }(),
			},
		},
	}

	noHintTool := ToolDefinition{
		Tool: mcp.Tool{
			Name: "no-hint-tool",
		},
	}

	toolset := Toolset{
		Name:        "Test Toolset",
		Description: "Test toolset",
		Tools:       []ToolDefinition{readOnlyTool, readWriteTool, noHintTool},
	}

	readOnlyTools := toolset.GetReadOnlyTools()

	require.Len(t, readOnlyTools, 1)
	assert.Equal(t, "read-only-tool", readOnlyTools[0].Tool.Name)
}

func TestToolset_GetAllTools(t *testing.T) {
	assert := require.New(t)

	tools := []ToolDefinition{
		{Tool: mcp.Tool{Name: "tool1"}},
		{Tool: mcp.Tool{Name: "tool2"}},
	}

	toolset := Toolset{
		Name:        "Test Toolset",
		Description: "Test toolset",
		Tools:       tools,
	}

	allTools := toolset.GetAllTools()
	assert.Equal(tools, allTools)
}

func TestToolset_GetRequiredScopes(t *testing.T) {
	assert := require.New(t)

	toolset := Toolset{
		Tools: []ToolDefinition{
			{RequiredScopes: []string{"read_builds", "write_builds"}},
			{RequiredScopes: []string{"read_pipelines", "read_builds"}},
			{RequiredScopes: []string{}},
		},
	}

	scopes := toolset.GetRequiredScopes()

	expected := []string{"read_builds", "read_pipelines", "write_builds"}
	assert.Equal(expected, scopes)
}

func TestNewToolsetRegistry(t *testing.T) {
	assert := require.New(t)

	registry := NewToolsetRegistry()

	assert.NotNil(t, registry)
	assert.NotNil(t, registry.toolsets)
	assert.Empty(registry.toolsets)
}

func TestToolsetRegistry_Register(t *testing.T) {
	assert := require.New(t)

	registry := NewToolsetRegistry()
	toolset := Toolset{
		Name:        "Test Toolset",
		Description: "A test toolset",
		Tools:       []ToolDefinition{},
	}

	registry.Register("test", toolset)

	retrievedToolset, exists := registry.toolsets["test"]
	assert.True(exists)
	assert.Equal(toolset, retrievedToolset)
}

func TestToolsetRegistry_RegisterToolsets(t *testing.T) {
	assert := require.New(t)

	registry := NewToolsetRegistry()
	toolsets := map[string]Toolset{
		"test1": {Name: "Test 1"},
		"test2": {Name: "Test 2"},
	}

	registry.RegisterToolsets(toolsets)

	assert.Len(registry.toolsets, 2)
	assert.Equal(toolsets["test1"], registry.toolsets["test1"])
	assert.Equal(toolsets["test2"], registry.toolsets["test2"])
}

func TestToolsetRegistry_Get(t *testing.T) {
	registry := NewToolsetRegistry()
	toolset := Toolset{Name: "Test Toolset"}
	registry.Register("test", toolset)

	t.Run("existing toolset", func(t *testing.T) {
		assert := require.New(t)
		result, exists := registry.Get("test")
		assert.True(exists)
		assert.Equal(toolset, result)
	})

	t.Run("non-existing toolset", func(t *testing.T) {
		assert := require.New(t)
		result, exists := registry.Get("nonexistent")
		assert.False(exists)
		assert.Equal(Toolset{}, result)
	})
}

func TestToolsetRegistry_List(t *testing.T) {
	registry := NewToolsetRegistry()

	t.Run("empty registry", func(t *testing.T) {
		assert := require.New(t)

		names := registry.List()
		assert.Empty(names)
	})

	t.Run("registry with toolsets", func(t *testing.T) {
		assert := require.New(t)

		registry.Register("zebra", Toolset{})
		registry.Register("alpha", Toolset{})
		registry.Register("beta", Toolset{})

		names := registry.List()
		expected := []string{"alpha", "beta", "zebra"}
		assert.Equal(expected, names)
	})
}

func TestToolsetRegistry_GetToolsForToolsets(t *testing.T) {
	registry := NewToolsetRegistry()

	readOnlyTool := ToolDefinition{
		Tool: mcp.Tool{
			Name: "read-only-tool",
			Annotations: mcp.ToolAnnotation{
				ReadOnlyHint: func() *bool { b := true; return &b }(),
			},
		},
	}

	readWriteTool := ToolDefinition{
		Tool: mcp.Tool{
			Name: "read-write-tool",
		},
	}

	anotherTool := ToolDefinition{
		Tool: mcp.Tool{
			Name: "another-tool",
		},
	}

	toolset1 := Toolset{
		Name:  "Toolset 1",
		Tools: []ToolDefinition{readOnlyTool, readWriteTool},
	}

	toolset2 := Toolset{
		Name:  "Toolset 2",
		Tools: []ToolDefinition{anotherTool},
	}

	registry.Register("toolset1", toolset1)
	registry.Register("toolset2", toolset2)

	t.Run("specific toolsets - all tools", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetToolsForToolsets([]string{"toolset1", "toolset2"}, false)
		assert.Len(tools, 3)

		toolNames := make([]string, len(tools))
		for i, tool := range tools {
			toolNames[i] = tool.Tool.Name
		}
		assert.Contains(toolNames, "read-only-tool")
		assert.Contains(toolNames, "read-write-tool")
		assert.Contains(toolNames, "another-tool")
	})

	t.Run("specific toolsets - read-only mode", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetToolsForToolsets([]string{"toolset1", "toolset2"}, true)
		assert.Len(tools, 1)
		assert.Equal("read-only-tool", tools[0].Tool.Name)
	})

	t.Run("single toolset", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetToolsForToolsets([]string{"toolset1"}, false)
		assert.Len(tools, 2)
	})

	t.Run("non-existent toolset", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetToolsForToolsets([]string{"nonexistent"}, false)
		assert.Empty(tools)
	})

	t.Run("empty toolset list", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetToolsForToolsets([]string{}, false)
		assert.Empty(tools)
	})

	t.Run("mixed valid and invalid toolsets", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetToolsForToolsets([]string{"toolset1", "nonexistent", "toolset2"}, false)
		assert.Len(tools, 3) // Only tools from valid toolsets
	})

	t.Run("scopes are available from returned tools", func(t *testing.T) {
		assert := require.New(t)

		// Create tools with specific scopes
		toolWithScopes := ToolDefinition{
			Tool: mcp.Tool{
				Name: "scoped-tool",
			},
			RequiredScopes: []string{"read_builds", "write_pipelines"},
		}

		anotherToolWithScopes := ToolDefinition{
			Tool: mcp.Tool{
				Name: "another-scoped-tool",
			},
			RequiredScopes: []string{"read_artifacts", "read_builds"},
		}

		scopedToolset := Toolset{
			Name:  "Scoped Toolset",
			Tools: []ToolDefinition{toolWithScopes, anotherToolWithScopes},
		}

		registry.Register("scoped", scopedToolset)

		tools := registry.GetToolsForToolsets([]string{"scoped"}, false)
		assert.Len(tools, 2)

		// Verify scopes are preserved
		var allScopes []string
		for _, tool := range tools {
			allScopes = append(allScopes, tool.RequiredScopes...)
		}

		assert.Contains(allScopes, "read_builds")
		assert.Contains(allScopes, "write_pipelines")
		assert.Contains(allScopes, "read_artifacts")

		// Verify specific tool scopes
		for _, tool := range tools {
			switch tool.Tool.Name {
			case "scoped-tool":
				assert.Equal([]string{"read_builds", "write_pipelines"}, tool.RequiredScopes)
			case "another-scoped-tool":
				assert.Equal([]string{"read_artifacts", "read_builds"}, tool.RequiredScopes)
			}
		}
	})
}

func TestToolsetRegistry_GetEnabledTools(t *testing.T) {
	registry := NewToolsetRegistry()

	readOnlyTool := ToolDefinition{
		Tool: mcp.Tool{
			Name: "read-only-tool",
			Annotations: mcp.ToolAnnotation{
				ReadOnlyHint: func() *bool { b := true; return &b }(),
			},
		},
	}

	readWriteTool := ToolDefinition{
		Tool: mcp.Tool{
			Name: "read-write-tool",
		},
	}

	toolset1 := Toolset{
		Name:  "Toolset 1",
		Tools: []ToolDefinition{readOnlyTool},
	}

	toolset2 := Toolset{
		Name:  "Toolset 2",
		Tools: []ToolDefinition{readWriteTool},
	}

	registry.Register("toolset1", toolset1)
	registry.Register("toolset2", toolset2)

	t.Run("specific toolsets - all tools", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetEnabledTools([]string{"toolset1", "toolset2"}, false)
		assert.Len(tools, 2)
	})

	t.Run("specific toolsets - read-only mode", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetEnabledTools([]string{"toolset1", "toolset2"}, true)
		assert.Len(tools, 1)
		assert.Equal("read-only-tool", tools[0].Tool.Name)
	})

	t.Run("all toolsets enabled", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetEnabledTools([]string{"all"}, false)
		assert.Len(tools, 2)
	})

	t.Run("non-existent toolset", func(t *testing.T) {
		assert := require.New(t)
		tools := registry.GetEnabledTools([]string{"nonexistent"}, false)
		assert.Empty(tools)
	})
}

func TestToolsetRegistry_GetMetadata(t *testing.T) {
	registry := NewToolsetRegistry()

	t.Run("single toolset", func(t *testing.T) {
		assert := require.New(t)
		readOnlyTool := ToolDefinition{
			Tool: mcp.Tool{
				Annotations: mcp.ToolAnnotation{
					ReadOnlyHint: func() *bool { b := true; return &b }(),
				},
			},
		}

		readWriteTool := ToolDefinition{
			Tool: mcp.Tool{},
		}

		toolset := Toolset{
			Name:        "Test Toolset",
			Description: "A test toolset",
			Tools:       []ToolDefinition{readOnlyTool, readWriteTool},
		}

		registry.Register("test", toolset)

		metadata := registry.GetMetadata()

		assert.Len(metadata, 1)
		assert.Equal("test", metadata[0].Name)
		assert.Equal("A test toolset", metadata[0].Description)
		assert.Equal(2, metadata[0].ToolCount)
		assert.Equal(1, metadata[0].ReadOnlyCount)
	})

	t.Run("multiple toolsets sorted by name", func(t *testing.T) {
		assert := require.New(t)
		// Clear registry for this test
		registry := NewToolsetRegistry()

		toolset1 := Toolset{
			Name:        "Zebra Toolset",
			Description: "Last in alphabetical order",
			Tools:       []ToolDefinition{{Tool: mcp.Tool{}}},
		}

		toolset2 := Toolset{
			Name:        "Alpha Toolset",
			Description: "First in alphabetical order",
			Tools: []ToolDefinition{
				{Tool: mcp.Tool{Annotations: mcp.ToolAnnotation{ReadOnlyHint: func() *bool { b := true; return &b }()}}},
				{Tool: mcp.Tool{}},
			},
		}

		toolset3 := Toolset{
			Name:        "Beta Toolset",
			Description: "Middle in alphabetical order",
			Tools:       []ToolDefinition{},
		}

		// Register in non-alphabetical order to test sorting
		registry.Register("zebra", toolset1)
		registry.Register("alpha", toolset2)
		registry.Register("beta", toolset3)

		metadata := registry.GetMetadata()

		assert.Len(metadata, 3)

		// Verify sorted by name (the metadata contains the registration key as Name)
		assert.Equal("alpha", metadata[0].Name)
		assert.Equal("First in alphabetical order", metadata[0].Description)
		assert.Equal(2, metadata[0].ToolCount)
		assert.Equal(1, metadata[0].ReadOnlyCount)

		assert.Equal("beta", metadata[1].Name)
		assert.Equal("Middle in alphabetical order", metadata[1].Description)
		assert.Equal(0, metadata[1].ToolCount)
		assert.Equal(0, metadata[1].ReadOnlyCount)

		assert.Equal("zebra", metadata[2].Name)
		assert.Equal("Last in alphabetical order", metadata[2].Description)
		assert.Equal(1, metadata[2].ToolCount)
		assert.Equal(0, metadata[2].ReadOnlyCount)
	})

	t.Run("empty registry", func(t *testing.T) {
		assert := require.New(t)
		emptyRegistry := NewToolsetRegistry()
		metadata := emptyRegistry.GetMetadata()
		assert.Empty(metadata)
	})
}

func TestToolsetRegistry_GetRequiredScopes(t *testing.T) {
	registry := NewToolsetRegistry()

	toolset1 := Toolset{
		Tools: []ToolDefinition{
			{RequiredScopes: []string{"read_builds", "write_builds"}},
		},
	}

	toolset2 := Toolset{
		Tools: []ToolDefinition{
			{RequiredScopes: []string{"read_pipelines", "read_builds"}},
		},
	}

	registry.Register("toolset1", toolset1)
	registry.Register("toolset2", toolset2)

	t.Run("specific toolsets", func(t *testing.T) {
		assert := require.New(t)
		scopes := registry.GetRequiredScopes([]string{"toolset1", "toolset2"}, false)
		expected := []string{"read_builds", "read_pipelines", "write_builds"}
		assert.Equal(expected, scopes)
	})

	t.Run("all toolsets", func(t *testing.T) {
		assert := require.New(t)
		scopes := registry.GetRequiredScopes([]string{"all"}, false)
		expected := []string{"read_builds", "read_pipelines", "write_builds"}
		assert.Equal(expected, scopes)
	})

	t.Run("read-only mode", func(t *testing.T) {
		assert := require.New(t)
		readOnlyTool := ToolDefinition{
			Tool: mcp.Tool{
				Annotations: mcp.ToolAnnotation{
					ReadOnlyHint: func() *bool { b := true; return &b }(),
				},
			},
			RequiredScopes: []string{"read_only"},
		}

		readWriteTool := ToolDefinition{
			Tool:           mcp.Tool{},
			RequiredScopes: []string{"write_only"},
		}

		toolsetMixed := Toolset{
			Tools: []ToolDefinition{readOnlyTool, readWriteTool},
		}

		registry.Register("mixed", toolsetMixed)

		scopes := registry.GetRequiredScopes([]string{"mixed"}, true)
		expected := []string{"read_only"}
		assert.Equal(expected, scopes)
	})
}

func TestNewTool(t *testing.T) {
	assert := require.New(t)

	mockHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		return &mcp.CallToolResult{}, nil
	}

	tool := mcp.Tool{Name: "test-tool"}
	scopes := []string{"read_test", "write_test"}

	toolDef := NewTool(tool, mockHandler, scopes)

	assert.Equal(tool, toolDef.Tool)
	assert.NotNil(toolDef.Handler)
	assert.Equal(scopes, toolDef.RequiredScopes)
}

func TestIsValidToolset(t *testing.T) {
	tests := []struct {
		name     string
		toolset  string
		expected bool
	}{
		{"valid toolset - all", "all", true},
		{"valid toolset - clusters", "clusters", true},
		{"valid toolset - pipelines", "pipelines", true},
		{"valid toolset - builds", "builds", true},
		{"valid toolset - artifacts", "artifacts", true},
		{"valid toolset - logs", "logs", true},
		{"valid toolset - tests", "tests", true},
		{"valid toolset - annotations", "annotations", true},
		{"valid toolset - user", "user", true},
		{"invalid toolset", "invalid", false},
		{"empty string", "", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := require.New(t)
			result := IsValidToolset(tt.toolset)
			assert.Equal(tt.expected, result)
		})
	}
}

func TestValidateToolsets(t *testing.T) {
	t.Run("all valid toolsets", func(t *testing.T) {
		assert := require.New(t)
		valid := []string{"all", "clusters", "pipelines"}
		err := ValidateToolsets(valid)
		assert.NoError(err)
	})

	t.Run("some invalid toolsets", func(t *testing.T) {
		assert := require.New(t)
		mixed := []string{"clusters", "invalid", "pipelines", "another-invalid"}
		err := ValidateToolsets(mixed)
		assert.Error(err)
		assert.Contains(err.Error(), "invalid")
		assert.Contains(err.Error(), "another-invalid")
	})

	t.Run("empty slice", func(t *testing.T) {
		assert := require.New(t)
		err := ValidateToolsets([]string{})
		assert.NoError(err)
	})
}

func TestCreateBuiltinToolsets(t *testing.T) {
	assert := require.New(t)

	client := &gobuildkite.Client{}

	registry := NewToolsetRegistry()
	builtin := CreateBuiltinToolsets(client, nil)
	registry.RegisterToolsets(builtin)

	// Check that expected toolsets are registered
	expectedToolsets := []string{"clusters", "pipelines", "builds", "artifacts", "logs", "tests", "annotations", "user"}
	for _, name := range expectedToolsets {
		_, exists := registry.Get(name)
		assert.True(exists, "expected toolset %s to be registered", name)
	}
}
