// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package registry

import (
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	types "github.com/stacklok/toolhive-core/registry/types"
	"github.com/stacklok/toolhive/pkg/config"
)

func TestNewRegistryProvider(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name         string
		config       *config.Config
		expectedType string
		expectError  bool
	}{
		{
			name:         "nil config returns embedded provider",
			config:       nil,
			expectedType: "*registry.LocalRegistryProvider",
			expectError:  false,
		},
		{
			name: "empty registry URL returns embedded provider",
			config: &config.Config{
				RegistryUrl: "",
			},
			expectedType: "*registry.LocalRegistryProvider",
			expectError:  false,
		},
		{
			name: "unreachable registry URL returns error",
			config: &config.Config{
				RegistryUrl: "https://non-existent-host-12345.com/registry.json",
			},
			expectedType: "",
			expectError:  true,
		},
		{
			name: "local registry path returns embedded provider with file path",
			config: &config.Config{
				LocalRegistryPath: "/path/to/registry.json",
			},
			expectedType: "*registry.LocalRegistryProvider",
			expectError:  false,
		},
	}

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

			if tt.expectError {
				assert.Error(t, err)
				assert.Nil(t, provider)
				return
			}

			assert.NoError(t, err)
			// Check the type of the provider
			providerType := getTypeName(provider)
			if providerType != tt.expectedType {
				t.Errorf("NewRegistryProvider() = %v, want %v", providerType, tt.expectedType)
			}
		})
	}
}

func TestLocalRegistryProvider(t *testing.T) {
	t.Parallel()
	provider := NewLocalRegistryProvider()

	// Test GetRegistry
	registry, err := provider.GetRegistry()
	if err != nil {
		t.Fatalf("GetRegistry() error = %v", err)
	}

	if registry == nil {
		t.Fatal("GetRegistry() returned nil registry")
		return
	}

	if len(registry.Servers) == 0 {
		t.Error("GetRegistry() returned registry with no servers")
	}

	// Test that server names are set
	for name, server := range registry.Servers {
		if server.Name != name {
			t.Errorf("ImageMetadata name not set correctly: got %s, want %s", server.Name, name)
		}
	}

	// Test ListServers
	servers, err := provider.ListServers()
	if err != nil {
		t.Fatalf("ListServers() error = %v", err)
	}

	totalServers := len(registry.Servers) + len(registry.RemoteServers)
	if len(servers) != totalServers {
		t.Errorf("ListServers() returned %d servers, want %d", len(servers), totalServers)
	}

	// Test GetServer with existing server
	if len(servers) > 0 {
		firstServer := servers[0]
		server, err := provider.GetServer(firstServer.GetName())
		if err != nil {
			t.Fatalf("GetServer() error = %v", err)
		}

		if server.GetName() != firstServer.GetName() {
			t.Errorf("GetServer() returned wrong server: got %s, want %s", server.GetName(), firstServer.GetName())
		}
	}

	// Test GetServer with non-existing server
	_, err = provider.GetServer("non-existing-server")
	if err == nil {
		t.Error("GetServer() with non-existing server should return error")
	}
}

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

	tests := []struct {
		name        string
		url         string
		expectError bool
	}{
		{
			name:        "invalid URL scheme",
			url:         "invalid://url",
			expectError: true,
		},
		{
			name:        "non-existent host",
			url:         "https://non-existent-host-12345.com/registry.json",
			expectError: true,
		},
	}

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

			provider, err := NewRemoteRegistryProvider(tt.url, false)

			if tt.expectError {
				assert.Error(t, err)
				assert.Nil(t, provider)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, provider)
				// Test that it implements the interface
				var _ Provider = provider
			}
		})
	}
}

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

	const upstreamWithServer = `{
		"$schema": "https://example.com/schema.json",
		"version": "1.0.0",
		"meta": {"last_updated": "2025-01-01T00:00:00Z"},
		"data": {
			"servers": [
				{
					"name": "io.example.test-server",
					"description": "Test server",
					"packages": [
						{
							"registryType": "oci",
							"identifier": "example/test-server:latest",
							"transport": {"type": "stdio"}
						}
					]
				}
			]
		}
	}`

	tests := []struct {
		name           string
		responseBody   string
		responseStatus int
		expectError    bool
		errorContains  string
	}{
		{
			name:           "valid upstream registry",
			responseBody:   upstreamWithServer,
			responseStatus: 200,
			expectError:    false,
		},
		{
			name:           "invalid JSON",
			responseBody:   `{"not valid json`,
			responseStatus: 200,
			expectError:    true,
			errorContains:  "invalid upstream JSON",
		},
		{
			name:           "upstream format with empty servers and groups",
			responseBody:   `{"$schema": "x", "version": "1.0", "meta": {}, "data": {"servers": []}}`,
			responseStatus: 200,
			expectError:    true,
			errorContains:  "no servers or groups",
		},
		{
			name:           "legacy format surfaces migration hint",
			responseBody:   `{"version": "1.0.0", "servers": {"x": {"image": "x:latest"}}}`,
			responseStatus: 200,
			expectError:    true,
			errorContains:  "legacy ToolHive format",
		},
		{
			name:           "non-200 status code",
			responseBody:   "Not Found",
			responseStatus: 404,
			expectError:    true,
			errorContains:  "status 404",
		},
	}

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

			// Create a test HTTP server that returns the specified response
			server := createTestServer(tt.responseBody, tt.responseStatus)
			defer server.Close()

			// Create provider with test server URL (allow private IPs for localhost)
			provider, err := NewRemoteRegistryProvider(server.URL, true)

			if tt.expectError {
				assert.Error(t, err)
				assert.Nil(t, provider)
				if tt.errorContains != "" {
					assert.Contains(t, err.Error(), tt.errorContains)
				}
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, provider)
			}
		})
	}
}

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

	// Create a temporary upstream-format registry file
	tempDir := t.TempDir()
	registryFile := filepath.Join(tempDir, "upstream_registry.json")

	testRegistry := `{
		"$schema": "https://cdn.mcpregistry.io/schema/v0/registry.json",
		"version": "1.0.0",
		"meta": {
			"last_updated": "2025-01-01T00:00:00Z"
		},
		"data": {
			"servers": [
				{
					"name": "io.example.test-server",
					"description": "Test server",
					"packages": [
						{
							"registryType": "oci",
							"identifier": "example/test-server:latest",
							"transport": {
								"type": "stdio"
							}
						}
					]
				}
			]
		}
	}`

	err := os.WriteFile(registryFile, []byte(testRegistry), 0644)
	require.NoError(t, err)

	provider := NewLocalRegistryProvider(registryFile)

	registry, err := provider.GetRegistry()
	require.NoError(t, err)
	require.NotNil(t, registry)

	assert.NotEmpty(t, registry.Servers, "Should have at least one container server")
}

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

	responseBody := `{
		"$schema": "https://cdn.mcpregistry.io/schema/v0/registry.json",
		"version": "1.0.0",
		"meta": {
			"last_updated": "2025-01-01T00:00:00Z"
		},
		"data": {
			"servers": [
				{
					"name": "io.example.test-server",
					"description": "Test server",
					"packages": [
						{
							"registryType": "oci",
							"identifier": "example/test-server:latest",
							"transport": {
								"type": "stdio"
							}
						}
					]
				}
			]
		}
	}`

	server := createTestServer(responseBody, 200)
	defer server.Close()

	provider, err := NewRemoteRegistryProvider(server.URL, true)
	require.NoError(t, err)
	require.NotNil(t, provider)

	registry, err := provider.GetRegistry()
	require.NoError(t, err)
	assert.NotEmpty(t, registry.Servers, "Should have at least one container server")
}

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

	// Build a controlled registry with known names
	reg := &types.Registry{
		Version:     "1.0.0",
		LastUpdated: "2025-01-01T00:00:00Z",
		Servers: map[string]*types.ImageMetadata{
			"io.github.stacklok/osv":    {BaseServerMetadata: types.BaseServerMetadata{Name: "io.github.stacklok/osv"}, Image: "ghcr.io/osv:latest"},
			"io.github.stacklok/github": {BaseServerMetadata: types.BaseServerMetadata{Name: "io.github.stacklok/github"}, Image: "ghcr.io/github:latest"},
			"io.github.acme/github":     {BaseServerMetadata: types.BaseServerMetadata{Name: "io.github.acme/github"}, Image: "ghcr.io/acme-github:latest"},
		},
		RemoteServers: map[string]*types.RemoteServerMetadata{
			"io.github.stacklok/slack-remote": {BaseServerMetadata: types.BaseServerMetadata{Name: "io.github.stacklok/slack-remote"}, URL: "https://slack.example.com"},
		},
	}

	provider := &LocalRegistryProvider{}
	provider.BaseProvider = NewBaseProvider(func() (*types.Registry, error) {
		return reg, nil
	})

	tests := []struct {
		name        string
		query       string
		expectName  string
		expectError string
	}{
		{
			name:       "exact full name match",
			query:      "io.github.stacklok/osv",
			expectName: "io.github.stacklok/osv",
		},
		{
			name:       "unique short name match",
			query:      "osv",
			expectName: "io.github.stacklok/osv",
		},
		{
			name:        "ambiguous short name errors with full names",
			query:       "github",
			expectError: "multiple servers match 'github'",
		},
		{
			name:        "ambiguous error lists both full names",
			query:       "github",
			expectError: "io.github.stacklok/github",
		},
		{
			name:        "ambiguous error lists both full names (second)",
			query:       "github",
			expectError: "io.github.acme/github",
		},
		{
			name:       "short name for remote server",
			query:      "slack-remote",
			expectName: "io.github.stacklok/slack-remote",
		},
		{
			name:        "no match returns not found",
			query:       "nonexistent",
			expectError: "server not found: nonexistent",
		},
		{
			name:        "partial name does not match (github-remote suffix check)",
			query:       "remote",
			expectError: "server not found: remote",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			server, err := provider.GetServer(tt.query)
			if tt.expectError != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.expectError)
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tt.expectName, server.GetName())
		})
	}
}

// getTypeName returns the type name of an interface value
func getTypeName(v interface{}) string {
	switch v.(type) {
	case *LocalRegistryProvider:
		return "*registry.LocalRegistryProvider"
	case *RemoteRegistryProvider:
		return "*registry.RemoteRegistryProvider"
	default:
		return "unknown"
	}
}

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

	// Create a temporary config for testing
	tempDir := t.TempDir()
	configPath := filepath.Join(tempDir, "toolhive", "config.yaml")

	// Ensure the directory exists
	err := os.MkdirAll(filepath.Dir(configPath), 0755)
	require.NoError(t, err)

	// Create a test config provider
	configProvider := config.NewPathProvider(configPath)

	// Create a test config
	cfg, err := configProvider.LoadOrCreateConfig()
	require.NoError(t, err)

	// Create provider with test config
	provider, err := NewRegistryProvider(cfg)
	require.NoError(t, err)
	reg, err := provider.GetRegistry()
	if err != nil {
		t.Fatalf("Failed to get registry: %v", err)
	}

	if reg == nil {
		t.Fatal("Registry is nil")
		return
	}

	if reg.Version == "" {
		t.Error("Registry version is empty")
	}

	if reg.LastUpdated == "" {
		t.Error("Registry last updated is empty")
	}

	if len(reg.Servers) == 0 {
		t.Error("Registry has no servers")
	}
}

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

	// Create a temporary config for testing
	tempDir := t.TempDir()
	configPath := filepath.Join(tempDir, "toolhive", "config.yaml")

	// Ensure the directory exists
	err := os.MkdirAll(filepath.Dir(configPath), 0755)
	require.NoError(t, err)

	// Create a test config provider
	configProvider := config.NewPathProvider(configPath)

	// Create a test config
	cfg, err := configProvider.LoadOrCreateConfig()
	require.NoError(t, err)

	// Create provider with test config
	provider, err := NewRegistryProvider(cfg)
	require.NoError(t, err)

	// Test getting an existing server (short name resolves via suffix match)
	server, err := provider.GetServer("osv")
	if err != nil {
		t.Fatalf("Failed to get server: %v", err)
	}

	if server == nil {
		t.Fatal("ServerMetadata is nil")
		return
	}

	// Check if it's a container server and has an image
	if !server.IsRemote() {
		if img, ok := server.(*types.ImageMetadata); ok {
			if img.Image == "" {
				t.Error("ImageMetadata image is empty")
			}
		}
	}

	if server.GetDescription() == "" {
		t.Error("ServerMetadata description is empty")
	}

	// Test getting a non-existent server
	_, err = provider.GetServer("non-existent-server")
	if err == nil {
		t.Error("Expected error when getting non-existent server")
	}
}

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

	// Create a temporary config for testing
	tempDir := t.TempDir()
	configPath := filepath.Join(tempDir, "toolhive", "config.yaml")

	// Ensure the directory exists
	err := os.MkdirAll(filepath.Dir(configPath), 0755)
	require.NoError(t, err)

	// Create a test config provider
	configProvider := config.NewPathProvider(configPath)

	// Create a test config
	cfg, err := configProvider.LoadOrCreateConfig()
	require.NoError(t, err)

	// Create provider with test config
	provider, err := NewRegistryProvider(cfg)
	require.NoError(t, err)

	// Test searching for servers
	servers, err := provider.SearchServers("search")
	if err != nil {
		t.Fatalf("Failed to search servers: %v", err)
	}

	if len(servers) == 0 {
		t.Error("No servers found for search query")
	}

	// Test searching for non-existent servers
	servers, err = provider.SearchServers("non-existent-server")
	if err != nil {
		t.Fatalf("Failed to search servers: %v", err)
	}

	if len(servers) > 0 {
		t.Errorf("Expected no servers for non-existent query, got %d", len(servers))
	}
}

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

	// Create a temporary config for testing
	tempDir := t.TempDir()
	configPath := filepath.Join(tempDir, "toolhive", "config.yaml")

	// Ensure the directory exists
	err := os.MkdirAll(filepath.Dir(configPath), 0755)
	require.NoError(t, err)

	// Create a test config provider
	configProvider := config.NewPathProvider(configPath)

	// Reset the default provider to ensure clean state
	ResetDefaultProvider()
	t.Cleanup(func() {
		ResetDefaultProvider()
	})

	provider, err := GetDefaultProviderWithConfig(configProvider)
	if err != nil {
		t.Fatalf("Failed to get registry provider: %v", err)
	}
	servers, err := provider.ListServers()
	if err != nil {
		t.Fatalf("Failed to list servers: %v", err)
	}

	if len(servers) == 0 {
		t.Error("No servers found")
	}

	// Verify that we get the same number of servers as in the registry
	reg, err := provider.GetRegistry()
	if err != nil {
		t.Fatalf("Failed to get registry: %v", err)
	}

	totalServers := len(reg.Servers) + len(reg.RemoteServers)
	if len(servers) != totalServers {
		t.Errorf("ListServers() returned %d servers, want %d", len(servers), totalServers)
	}
}

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

	// Test with non-existent file path
	provider := NewLocalRegistryProvider("/non/existent/path/registry.json")

	registry, err := provider.GetRegistry()

	assert.Error(t, err)
	assert.Nil(t, registry)
	assert.Contains(t, err.Error(), "failed to read local registry file")
}

// TestLocalRegistryProvider_LegacyFileReturnsMigrationHint covers the upgrade
// scenario: a user with a legacy --local-registry-path on disk should get a
// clear migration hint, not an empty registry.
func TestLocalRegistryProvider_LegacyFileReturnsMigrationHint(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	path := filepath.Join(dir, "registry.json")
	require.NoError(t, os.WriteFile(path, []byte(`{
		"version": "1.0.0",
		"servers": {"test": {"image": "test:latest"}}
	}`), 0o600))

	provider := NewLocalRegistryProvider(path)
	registry, err := provider.GetRegistry()

	require.Error(t, err)
	assert.Nil(t, registry)
	assert.ErrorIs(t, err, errLegacyFormat)
}

// createTestServer creates a test HTTP server that returns the specified response
func createTestServer(responseBody string, statusCode int) *httptest.Server {
	handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(statusCode)
		_, _ = w.Write([]byte(responseBody))
	})

	return httptest.NewServer(handler)
}
