package filtering

import (
	"context"
	"strings"
	"testing"

	upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
	"github.com/modelcontextprotocol/registry/pkg/model"
	toolhivetypes "github.com/stacklok/toolhive-core/registry/types"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/stacklok/toolhive-registry-server/internal/config"
	"github.com/stacklok/toolhive-registry-server/internal/registry"
)

// Helper to assert servers contain expected names
func assertContainsServerNames(t *testing.T, servers []upstreamv0.ServerJSON, expectedNames []string) {
	t.Helper()
	assert.Len(t, servers, len(expectedNames), "Server count mismatch")
	for _, expectedName := range expectedNames {
		found := false
		for _, server := range servers {
			if strings.Contains(server.Name, expectedName) {
				found = true
				break
			}
		}
		assert.True(t, found, "Expected server %s in results", expectedName)
	}
}

func TestNewDefaultFilterService(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	assert.NotNil(t, service)
	// Cast to concrete type to access fields in tests (same package)
	concreteService := service.(*defaultFilterService)
	assert.NotNil(t, concreteService.nameFilter)
	assert.NotNil(t, concreteService.tagFilter)
}

func TestNewFilterService(t *testing.T) {
	t.Parallel()

	nameFilter := NewDefaultNameFilter()
	tagFilter := NewDefaultTagFilter()

	service := NewFilterService(nameFilter, tagFilter)
	assert.NotNil(t, service)
	// Cast to concrete type to access fields in tests (same package)
	concreteService := service.(*defaultFilterService)
	assert.Equal(t, nameFilter, concreteService.nameFilter)
	assert.Equal(t, tagFilter, concreteService.tagFilter)
}

func TestDefaultFilterService_ApplyFilters_NoFilter(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	// Create test registry with both container and remote servers
	originalRegistry := registry.NewTestUpstreamRegistry(
		registry.WithServers(
			registry.NewTestServer("postgres",
				registry.WithNamespace("io.test/"),
				registry.WithTags("database", "sql"),
				registry.WithOCIPackage("postgres:latest"),
				registry.WithServerVersion("1.0.0"),
			),
			registry.NewTestServer("web-api",
				registry.WithNamespace("io.test/"),
				registry.WithTags("web", "api"),
				registry.WithHTTPPackage("https://example.com"),
				registry.WithMetadata("test", "test"),
				registry.WithToolHiveMetadata("tier", "Official"),
			),
		),
	)

	// Apply no filter
	result, err := service.ApplyFilters(ctx, originalRegistry, nil)
	require.NoError(t, err)
	assert.Equal(t, originalRegistry, result, "No filter should return original registry")
}

func TestDefaultFilterService_ApplyFilters_NameFiltering(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	tests := []struct {
		name          string
		nameInclude   []string
		nameExclude   []string
		expectedNames []string // Unified: all server names that should be in result
	}{
		{
			name:          "include postgres pattern",
			nameInclude:   []string{"*/postgres-*"},
			nameExclude:   []string{},
			expectedNames: []string{"postgres-server"},
		},
		{
			name:          "exclude experimental pattern",
			nameInclude:   []string{},
			nameExclude:   []string{"*/*-experimental"},
			expectedNames: []string{"postgres-server", "mysql-server", "web-api"},
		},
		{
			name:          "include with exclude precedence",
			nameInclude:   []string{"*/*-server"},
			nameExclude:   []string{"*/*-experimental"},
			expectedNames: []string{"postgres-server", "mysql-server"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Create test registry with both container and remote servers
			originalRegistry := registry.NewTestUpstreamRegistry(
				registry.WithServers(
					registry.NewTestServer("postgres-server",
						registry.WithNamespace("io.test/"),
						registry.WithTags("database"),
						registry.WithOCIPackage("postgres:latest"),
					),
					registry.NewTestServer("mysql-server",
						registry.WithNamespace("io.test/"),
						registry.WithTags("database"),
						registry.WithOCIPackage("mysql:latest"),
					),
					registry.NewTestServer("redis-experimental",
						registry.WithNamespace("io.test/"),
						registry.WithTags("cache"),
						registry.WithOCIPackage("redis:latest"),
					),
					registry.NewTestServer("web-api",
						registry.WithNamespace("io.test/"),
						registry.WithTags("web"),
						registry.WithHTTPPackage("https://example.com"),
					),
					registry.NewTestServer("admin-experimental",
						registry.WithNamespace("io.test/"),
						registry.WithTags("admin"),
						registry.WithHTTPPackage("https://admin.example.com"),
					),
				),
			)

			filter := &config.FilterConfig{
				Names: &config.NameFilterConfig{
					Include: tt.nameInclude,
					Exclude: tt.nameExclude,
				},
			}

			result, err := service.ApplyFilters(ctx, originalRegistry, filter)

			require.NoError(t, err)
			assert.Equal(t, originalRegistry.Version, result.Version)
			assert.Equal(t, originalRegistry.Meta.LastUpdated, result.Meta.LastUpdated)

			// Check filtered servers using helper
			assertContainsServerNames(t, result.Data.Servers, tt.expectedNames)
		})
	}
}

func TestDefaultFilterService_ApplyFilters_TagFiltering(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	tests := []struct {
		name          string
		tagInclude    []string
		tagExclude    []string
		expectedNames []string // Unified: all server names that should be in result
	}{
		{
			name:          "include database tags",
			tagInclude:    []string{"database"},
			tagExclude:    []string{},
			expectedNames: []string{"postgres-server", "mysql-server"},
		},
		{
			name:          "exclude deprecated tags",
			tagInclude:    []string{},
			tagExclude:    []string{"deprecated"},
			expectedNames: []string{"postgres-server", "redis-server", "web-api"},
		},
		{
			name:          "include with exclude precedence",
			tagInclude:    []string{"database"},
			tagExclude:    []string{"deprecated"},
			expectedNames: []string{"postgres-server"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Create test registry with servers having different tags
			originalRegistry := registry.NewTestUpstreamRegistry(
				registry.WithServers(
					registry.NewTestServer("postgres-server",
						registry.WithNamespace("io.test/"),
						registry.WithTags("database", "sql"),
						registry.WithOCIPackage("postgres:latest"),
					),
					registry.NewTestServer("mysql-server",
						registry.WithNamespace("io.test/"),
						registry.WithTags("database", "deprecated"),
						registry.WithOCIPackage("mysql:latest"),
					),
					registry.NewTestServer("redis-server",
						registry.WithNamespace("io.test/"),
						registry.WithTags("cache"),
						registry.WithOCIPackage("redis:latest"),
					),
					registry.NewTestServer("web-api",
						registry.WithNamespace("io.test/"),
						registry.WithTags("web", "api"),
						registry.WithHTTPPackage("https://example.com"),
					),
					registry.NewTestServer("legacy-api",
						registry.WithNamespace("io.test/"),
						registry.WithTags("web", "deprecated"),
						registry.WithHTTPPackage("https://legacy.example.com"),
					),
				),
			)

			filter := &config.FilterConfig{
				Tags: &config.TagFilterConfig{
					Include: tt.tagInclude,
					Exclude: tt.tagExclude,
				},
			}

			result, err := service.ApplyFilters(ctx, originalRegistry, filter)

			require.NoError(t, err)

			// Check filtered servers using helper
			assertContainsServerNames(t, result.Data.Servers, tt.expectedNames)
		})
	}
}

func TestDefaultFilterService_ApplyFilters_CombinedFiltering(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	// Create test registry
	originalRegistry := registry.NewTestUpstreamRegistry(
		registry.WithServers(
			registry.NewTestServer("postgres-server",
				registry.WithNamespace("io.test/"),
				registry.WithTags("database", "sql"),
				registry.WithOCIPackage("postgres:latest"),
			),
			registry.NewTestServer("postgres-experimental",
				registry.WithNamespace("io.test/"),
				registry.WithTags("database", "experimental"),
				registry.WithOCIPackage("postgres:experimental"),
			),
			registry.NewTestServer("web-server",
				registry.WithNamespace("io.test/"),
				registry.WithTags("web", "api"),
				registry.WithOCIPackage("nginx:latest"),
			),
			registry.NewTestServer("database-api",
				registry.WithNamespace("io.test/"),
				registry.WithTags("database", "api"),
				registry.WithHTTPPackage("https://db.example.com"),
			),
		),
	)

	filter := &config.FilterConfig{
		Names: &config.NameFilterConfig{
			Include: []string{"*/postgres-*", "*/*-api"},
			Exclude: []string{"*/*-experimental"},
		},
		Tags: &config.TagFilterConfig{
			Include: []string{"database"},
			Exclude: []string{},
		},
	}

	result, err := service.ApplyFilters(ctx, originalRegistry, filter)

	require.NoError(t, err)

	// Should include:
	// - postgres-server: matches postgres-* pattern AND has database tag
	// - database-api: matches *-api pattern AND has database tag
	// Should exclude:
	// - postgres-experimental: excluded by *-experimental pattern (exclude takes precedence)
	// - web-server: matches no name patterns
	assertContainsServerNames(t, result.Data.Servers, []string{"postgres-server", "database-api"})
}

func TestDefaultFilterService_ApplyFilters_EmptyRegistry(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	// Create empty registry
	originalRegistry := registry.NewTestUpstreamRegistry()

	filter := &config.FilterConfig{
		Names: &config.NameFilterConfig{
			Include: []string{"*/postgres-*"},
		},
	}

	result, err := service.ApplyFilters(ctx, originalRegistry, filter)

	require.NoError(t, err)
	assert.Len(t, result.Data.Servers, 0)
	assert.Equal(t, originalRegistry.Version, result.Version)
	assert.Equal(t, originalRegistry.Meta.LastUpdated, result.Meta.LastUpdated)
}

func TestDefaultFilterService_ApplyFilters_PreservesMetadata(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	// Create registry with specific metadata values
	originalRegistry := registry.NewTestUpstreamRegistry(
		registry.WithVersion("2.5.3"),
		registry.WithLastUpdated("2023-12-15T14:30:00Z"),
		registry.WithServers(
			registry.NewTestServer("test-server",
				registry.WithNamespace("io.test/"),
				registry.WithTags("test"),
				registry.WithOCIPackage("test:latest"),
			),
		),
	)

	filter := &config.FilterConfig{
		Names: &config.NameFilterConfig{
			Include: []string{"*/*"},
		},
	}

	result, err := service.ApplyFilters(ctx, originalRegistry, filter)

	require.NoError(t, err)
	// Verify metadata is preserved exactly
	assert.Equal(t, originalRegistry.Version, result.Version)
	assert.Equal(t, originalRegistry.Meta.LastUpdated, result.Meta.LastUpdated)
	// Verify server is included (wildcard match)
	assert.Len(t, result.Data.Servers, 1)
}

// TestDefaultFilterService_ApplyFilters_UpstreamFormatWithStringMetadata reproduces issue #299
// where upstream format servers with string values in PublisherProvided cause a panic
// when filters are applied.
func TestDefaultFilterService_ApplyFilters_UpstreamFormatWithStringMetadata(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	// Create an upstream format server with PublisherProvided containing a string value
	// This simulates the structure from upstream MCP registry API where some metadata
	// values might be strings instead of nested maps
	upstreamServer := upstreamv0.ServerJSON{
		Schema:      "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
		Name:        "ai.smithery/test-server",
		Description: "Test server from upstream",
		Version:     "1.0.0",
		Packages:    []model.Package{},
		Meta: &upstreamv0.ServerMeta{
			PublisherProvided: map[string]any{
				// This is a string value, not a map - this causes the panic in ExtractTags
				"someKey": "someStringValue",
				// This is a map (expected structure)
				"provider": map[string]any{
					"metadata": map[string]any{
						"tags": []any{"tag1", "tag2"},
					},
				},
			},
		},
	}

	originalRegistry := registry.NewTestUpstreamRegistry(
		registry.WithServers(upstreamServer),
	)

	// Apply a name filter (even without tag filter, ExtractTags is called)
	filter := &config.FilterConfig{
		Names: &config.NameFilterConfig{
			Exclude: []string{"ai.smithery/*"},
		},
	}

	// This should not panic - ExtractTags should handle string values gracefully
	result, err := service.ApplyFilters(ctx, originalRegistry, filter)

	require.NoError(t, err)
	// Server should be excluded by name filter
	assert.Len(t, result.Data.Servers, 0, "Server should be excluded by name filter")
}

func TestDefaultFilterService_ApplyFilters_FiltersSkillsByName(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	originalRegistry := registry.NewTestUpstreamRegistry(
		registry.WithServers(
			registry.NewTestServer("test-server",
				registry.WithNamespace("io.test/"),
				registry.WithTags("test"),
				registry.WithOCIPackage("test:latest"),
			),
		),
	)
	// Add skills directly since there's no builder option for them
	originalRegistry.Data.Skills = []toolhivetypes.Skill{
		{
			Namespace:   "io.github.test",
			Name:        "pdf-processor",
			Version:     "1.0.0",
			Description: "Test skill",
		},
		{
			Namespace:   "io.github.test",
			Name:        "code-review",
			Version:     "2.0.0",
			Description: "Another skill",
		},
		{
			Namespace:   "io.github.other",
			Name:        "data-export",
			Version:     "1.0.0",
			Description: "Other namespace skill",
		},
	}

	// Exclude server namespace and one skill namespace — keep only io.github.other
	filter := &config.FilterConfig{
		Names: &config.NameFilterConfig{
			Exclude: []string{"io.test/*", "io.github.test/*"},
		},
	}

	result, err := service.ApplyFilters(ctx, originalRegistry, filter)

	require.NoError(t, err)
	// Server should be excluded
	assert.Empty(t, result.Data.Servers)
	// Only the io.github.other skill should remain
	require.Len(t, result.Data.Skills, 1)
	assert.Equal(t, "data-export", result.Data.Skills[0].Name)
	assert.Equal(t, "io.github.other", result.Data.Skills[0].Namespace)
}

func TestDefaultFilterService_ApplyFilters_SkillsPassWithNoFilter(t *testing.T) {
	t.Parallel()

	service := NewDefaultFilterService()
	ctx := context.Background()

	originalRegistry := registry.NewTestUpstreamRegistry()
	originalRegistry.Data.Skills = []toolhivetypes.Skill{
		{
			Namespace:   "io.github.test",
			Name:        "pdf-processor",
			Version:     "1.0.0",
			Description: "Test skill",
		},
	}

	result, err := service.ApplyFilters(ctx, originalRegistry, nil)

	require.NoError(t, err)
	require.Len(t, result.Data.Skills, 1)
	assert.Equal(t, "pdf-processor", result.Data.Skills[0].Name)
}
