package main

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/modelcontextprotocol/go-sdk/mcp"

	"github.com/txn2/mcp-data-platform/pkg/health"
	"github.com/txn2/mcp-data-platform/pkg/persona"
	"github.com/txn2/mcp-data-platform/pkg/platform"
	"github.com/txn2/mcp-data-platform/pkg/portal"
	"github.com/txn2/mcp-data-platform/pkg/session"
)

const (
	testSessionTimeout = 10 * time.Minute
	testGracePeriod20s = 20 * time.Second
	testPreDelay3s     = 3 * time.Second
)

func TestInitLogging(t *testing.T) {
	tests := []struct {
		env   string
		level slog.Level
	}{
		{"debug", slog.LevelDebug},
		{"DEBUG", slog.LevelDebug},
		{"info", slog.LevelInfo},
		{"warn", slog.LevelWarn},
		{"WARN", slog.LevelWarn},
		{"error", slog.LevelError},
		{"ERROR", slog.LevelError},
		{"", slog.LevelInfo},        // default
		{"unknown", slog.LevelInfo}, // unrecognized falls through
	}

	for _, tt := range tests {
		t.Run("LOG_LEVEL="+tt.env, func(t *testing.T) {
			t.Setenv("LOG_LEVEL", tt.env)
			initLogging()

			handler := slog.Default().Handler()
			// Verify the handler is enabled at the expected level
			if !handler.Enabled(context.Background(), tt.level) {
				t.Errorf("expected handler enabled at %v", tt.level)
			}
			// For non-debug levels, debug should be disabled
			if tt.level > slog.LevelDebug && handler.Enabled(context.Background(), slog.LevelDebug) {
				t.Errorf("expected debug disabled when LOG_LEVEL=%q", tt.env)
			}
		})
	}
}

func TestRegisterOAuthRoutes(t *testing.T) {
	mux := http.NewServeMux()
	handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	registerOAuthRoutes(mux, handler)

	// Test all registered routes
	routes := []string{
		// Standard paths (with /oauth prefix)
		"/.well-known/oauth-authorization-server",
		"/oauth/authorize",
		"/oauth/callback",
		"/oauth/token",
		"/oauth/register",
		// Claude Desktop compatibility paths (without /oauth prefix)
		"/authorize",
		"/callback",
		"/token",
		"/register",
	}

	for _, route := range routes {
		t.Run(route, func(t *testing.T) {
			req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, route, http.NoBody)
			w := httptest.NewRecorder()
			mux.ServeHTTP(w, req)

			if w.Code != http.StatusOK {
				t.Errorf("route %s: expected status 200, got %d", route, w.Code)
			}
		})
	}
}

func TestCorsMiddleware(t *testing.T) {
	inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	handler := corsMiddleware(inner)

	t.Run("sets CORS headers", func(t *testing.T) {
		req := httptest.NewRequestWithContext(context.Background(), "GET", "/", http.NoBody)
		req.Header.Set("Origin", "https://example.com")
		w := httptest.NewRecorder()

		handler.ServeHTTP(w, req)

		if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://example.com" {
			t.Errorf("Allow-Origin = %q, want %q", got, "https://example.com")
		}

		methods := w.Header().Get("Access-Control-Allow-Methods")
		for _, m := range []string{"GET", "POST", "DELETE", "OPTIONS"} {
			if !strings.Contains(methods, m) {
				t.Errorf("Allow-Methods missing %q: %s", m, methods)
			}
		}

		allowHeaders := w.Header().Get("Access-Control-Allow-Headers")
		for _, h := range []string{"Mcp-Session-Id", "Mcp-Protocol-Version", "X-API-Key", "Last-Event-ID"} {
			if !strings.Contains(allowHeaders, h) {
				t.Errorf("Allow-Headers missing %q: %s", h, allowHeaders)
			}
		}

		exposeHeaders := w.Header().Get("Access-Control-Expose-Headers")
		if !strings.Contains(exposeHeaders, "Mcp-Session-Id") {
			t.Errorf("Expose-Headers missing Mcp-Session-Id: %s", exposeHeaders)
		}

		if got := w.Header().Get("Access-Control-Allow-Credentials"); got != "true" {
			t.Errorf("Allow-Credentials = %q, want %q", got, "true")
		}
	})

	t.Run("handles OPTIONS preflight", func(t *testing.T) {
		req := httptest.NewRequestWithContext(context.Background(), "OPTIONS", "/mcp", http.NoBody)
		req.Header.Set("Origin", "https://example.com")
		w := httptest.NewRecorder()

		handler.ServeHTTP(w, req)

		if w.Code != http.StatusOK {
			t.Errorf("OPTIONS status = %d, want %d", w.Code, http.StatusOK)
		}
	})

	t.Run("defaults origin to wildcard", func(t *testing.T) {
		req := httptest.NewRequestWithContext(context.Background(), "GET", "/", http.NoBody)
		w := httptest.NewRecorder()

		handler.ServeHTTP(w, req)

		if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
			t.Errorf("Allow-Origin = %q, want %q", got, "*")
		}
	})
}

func TestStartServer_UnknownTransport(t *testing.T) {
	err := startServer(context.TODO(), nil, nil, serverOptions{transport: "websocket"})
	if err == nil {
		t.Fatal("expected error for unknown transport")
	}
	if !strings.Contains(err.Error(), "unknown transport") {
		t.Errorf("error = %q, want 'unknown transport'", err.Error())
	}
}

func TestExtractHTTPConfig_NilPlatform(t *testing.T) {
	cfg := extractHTTPConfig(nil)
	if cfg.requireAuth {
		t.Error("expected requireAuth false for nil platform")
	}
	if cfg.tlsEnabled {
		t.Error("expected tlsEnabled false for nil platform")
	}
	if cfg.tlsCertFile != "" {
		t.Error("expected empty tlsCertFile for nil platform")
	}
	if cfg.tlsKeyFile != "" {
		t.Error("expected empty tlsKeyFile for nil platform")
	}
	if cfg.streamableCfg.Stateless {
		t.Error("expected stateless false for nil platform")
	}
}

func TestExtractHTTPConfig_WithPlatform(t *testing.T) {
	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{
			Name: "test",
			TLS: platform.TLSConfig{
				Enabled:  true,
				CertFile: "/cert.pem",
				KeyFile:  "/key.pem",
			},
			Streamable: platform.StreamableConfig{
				SessionTimeout: testSessionTimeout,
				Stateless:      true,
			},
		},
		Auth: platform.AuthConfig{
			AllowAnonymous: false,
		},
	})
	defer func() { _ = p.Close() }()

	cfg := extractHTTPConfig(p)
	if !cfg.requireAuth {
		t.Error("expected requireAuth true")
	}
	if !cfg.tlsEnabled {
		t.Error("expected tlsEnabled true")
	}
	if cfg.tlsCertFile != "/cert.pem" {
		t.Errorf("tlsCertFile = %q, want /cert.pem", cfg.tlsCertFile)
	}
	if cfg.tlsKeyFile != "/key.pem" {
		t.Errorf("tlsKeyFile = %q, want /key.pem", cfg.tlsKeyFile)
	}
	if cfg.streamableCfg.SessionTimeout != testSessionTimeout {
		t.Errorf("sessionTimeout = %v, want 10m", cfg.streamableCfg.SessionTimeout)
	}
	if !cfg.streamableCfg.Stateless {
		t.Error("expected stateless true")
	}
}

func TestExtractHTTPConfig_AllowAnonymous(t *testing.T) {
	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{Name: "test"},
		Auth:   platform.AuthConfig{AllowAnonymous: true},
	})
	defer func() { _ = p.Close() }()

	cfg := extractHTTPConfig(p)
	if cfg.requireAuth {
		t.Error("expected requireAuth false when AllowAnonymous is true")
	}
}

func TestNewSSEHandler(t *testing.T) {
	mcpServer := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)

	t.Run("without auth", func(t *testing.T) {
		handler := newSSEHandler(mcpServer, false, "")
		if handler == nil {
			t.Fatal("expected non-nil handler")
		}
	})

	t.Run("with auth no OAuth", func(t *testing.T) {
		handler := newSSEHandler(mcpServer, true, "")
		if handler == nil {
			t.Fatal("expected non-nil handler")
		}
	})

	t.Run("with auth and OAuth", func(t *testing.T) {
		handler := newSSEHandler(mcpServer, true, "https://mcp.example.com/.well-known/oauth-protected-resource")
		if handler == nil {
			t.Fatal("expected non-nil handler")
		}
	})
}

func TestResourceMetadataURL(t *testing.T) {
	t.Run("returns empty for nil platform", func(t *testing.T) {
		if got := resourceMetadataURL(nil); got != "" {
			t.Errorf("resourceMetadataURL(nil) = %q, want empty", got)
		}
	})

	t.Run("returns empty when OAuth not enabled", func(t *testing.T) {
		p := newTestPlatform(t, &platform.Config{
			Server: platform.ServerConfig{Name: "test"},
		})
		defer func() { _ = p.Close() }()

		if got := resourceMetadataURL(p); got != "" {
			t.Errorf("resourceMetadataURL = %q, want empty (OAuth not enabled)", got)
		}
	})

	t.Run("returns URL when OAuth enabled", func(t *testing.T) {
		p := newTestPlatform(t, &platform.Config{
			Server: platform.ServerConfig{Name: "test"},
			OAuth: platform.OAuthConfig{
				Enabled:    true,
				Issuer:     "https://mcp.example.com",
				SigningKey: "dGVzdC1zaWduaW5nLWtleS0xMjM0NTY3ODkwYWJjZGVm", // base64, 33 bytes
			},
		})
		defer func() { _ = p.Close() }()

		want := "https://mcp.example.com/.well-known/oauth-protected-resource"
		if got := resourceMetadataURL(p); got != want {
			t.Errorf("resourceMetadataURL = %q, want %q", got, want)
		}
	})
}

func TestListenAndServe_GracefulShutdown(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	hc := health.NewChecker()
	hcfg := httpConfig{
		shutdownCfg: platform.ShutdownConfig{
			GracePeriod:      1 * time.Second,
			PreShutdownDelay: 0, // skip delay in tests
		},
	}

	errCh := make(chan error, 1)
	go func() {
		errCh <- listenAndServe(ctx, "127.0.0.1:0", handler, hcfg, hc)
	}()

	// Give the server a moment to start, then cancel.
	time.Sleep(50 * time.Millisecond)

	if !hc.IsReady() {
		t.Error("health checker should be ready after server starts")
	}

	cancel()

	select {
	case err := <-errCh:
		if err != nil {
			t.Fatalf("listenAndServe returned error: %v", err)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("listenAndServe did not shut down in time")
	}

	if hc.IsReady() {
		t.Error("health checker should not be ready after shutdown")
	}
}

func TestListenAndServe_TLSBadCert(t *testing.T) {
	ctx := t.Context()

	handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	hcfg := httpConfig{
		tlsEnabled:  true,
		tlsCertFile: "/nonexistent/cert.pem",
		tlsKeyFile:  "/nonexistent/key.pem",
	}

	err := listenAndServe(ctx, "127.0.0.1:0", handler, hcfg, nil)
	if err == nil {
		t.Fatal("expected error for bad TLS cert path")
	}
}

func TestListenAndServe_NilHealthChecker(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	errCh := make(chan error, 1)
	go func() {
		errCh <- listenAndServe(ctx, "127.0.0.1:0", handler, httpConfig{}, nil)
	}()

	time.Sleep(50 * time.Millisecond)
	cancel()

	select {
	case err := <-errCh:
		if err != nil {
			t.Fatalf("listenAndServe returned error: %v", err)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("listenAndServe did not shut down in time")
	}
}

func TestStartHTTPServer_GracefulShutdown(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	mcpServer := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)

	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{
			Name: "test",
		},
		Auth: platform.AuthConfig{AllowAnonymous: true},
	})
	defer func() { _ = p.Close() }()

	errCh := make(chan error, 1)
	go func() {
		errCh <- startHTTPServer(ctx, mcpServer, p, serverOptions{address: "127.0.0.1:0"})
	}()

	time.Sleep(50 * time.Millisecond)
	cancel()

	select {
	case err := <-errCh:
		if err != nil {
			t.Fatalf("startHTTPServer returned error: %v", err)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("startHTTPServer did not shut down in time")
	}
}

func TestStartServer_HTTPBackwardCompat(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	mcpServer := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)

	errCh := make(chan error, 1)
	go func() {
		errCh <- startServer(ctx, mcpServer, nil, serverOptions{
			transport: "sse",
			address:   "127.0.0.1:0",
		})
	}()

	time.Sleep(50 * time.Millisecond)
	cancel()

	select {
	case err := <-errCh:
		if err != nil {
			t.Fatalf("startServer with 'sse' transport returned error: %v", err)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("startServer did not shut down in time")
	}
}

func TestExtractHTTPConfig_ShutdownConfig(t *testing.T) {
	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{
			Name: "test",
			Shutdown: platform.ShutdownConfig{
				GracePeriod:      testGracePeriod20s,
				PreShutdownDelay: testPreDelay3s,
			},
		},
	})
	defer func() { _ = p.Close() }()

	cfg := extractHTTPConfig(p)
	if cfg.shutdownCfg.GracePeriod != testGracePeriod20s {
		t.Errorf("shutdownCfg.GracePeriod = %v, want %v", cfg.shutdownCfg.GracePeriod, testGracePeriod20s)
	}
	if cfg.shutdownCfg.PreShutdownDelay != testPreDelay3s {
		t.Errorf("shutdownCfg.PreShutdownDelay = %v, want %v", cfg.shutdownCfg.PreShutdownDelay, testPreDelay3s)
	}
}

func TestHealthEndpointsRegistered(t *testing.T) {
	mux := http.NewServeMux()
	hc := health.NewChecker()
	mux.Handle("/healthz", hc.LivenessHandler())
	mux.Handle("/readyz", hc.ReadinessHandler())

	// Set ready (simulating what listenAndServe does)
	hc.SetReady()

	// Test /healthz
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, httptest.NewRequestWithContext(context.Background(), "GET", "/healthz", http.NoBody))
	if w.Code != http.StatusOK {
		t.Errorf("/healthz status = %d, want %d", w.Code, http.StatusOK)
	}

	// Test /readyz when ready
	w = httptest.NewRecorder()
	mux.ServeHTTP(w, httptest.NewRequestWithContext(context.Background(), "GET", "/readyz", http.NoBody))
	if w.Code != http.StatusOK {
		t.Errorf("/readyz status = %d, want %d (ready)", w.Code, http.StatusOK)
	}

	// Test /readyz when draining
	hc.SetDraining()
	w = httptest.NewRecorder()
	mux.ServeHTTP(w, httptest.NewRequestWithContext(context.Background(), "GET", "/readyz", http.NoBody))
	if w.Code != http.StatusServiceUnavailable {
		t.Errorf("/readyz status = %d, want %d (draining)", w.Code, http.StatusServiceUnavailable)
	}
}

func TestRobotsTxt(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/plain")
		_, _ = fmt.Fprint(w, "User-agent: *\nDisallow: /\n")
	})

	w := httptest.NewRecorder()
	mux.ServeHTTP(w, httptest.NewRequestWithContext(context.Background(), "GET", "/robots.txt", http.NoBody))

	if w.Code != http.StatusOK {
		t.Fatalf("/robots.txt status = %d, want %d", w.Code, http.StatusOK)
	}
	if ct := w.Header().Get("Content-Type"); ct != "text/plain" {
		t.Errorf("Content-Type = %q, want %q", ct, "text/plain")
	}
	body := w.Body.String()
	if !strings.Contains(body, "Disallow: /") {
		t.Errorf("body missing Disallow directive: %q", body)
	}
}

func TestAwareHandlerWiring(t *testing.T) {
	t.Run("not wired when stateless is false", func(t *testing.T) {
		p := newTestPlatform(t, &platform.Config{
			Server: platform.ServerConfig{
				Name: "test",
				Streamable: platform.StreamableConfig{
					Stateless: false,
				},
			},
		})
		defer func() { _ = p.Close() }()

		hcfg := extractHTTPConfig(p)

		// SessionStore is non-nil (memory), but Stateless is false.
		if p.SessionStore() == nil {
			t.Fatal("expected non-nil session store")
		}
		if hcfg.streamableCfg.Stateless {
			t.Error("expected Stateless false for memory mode")
		}
	})

	t.Run("wired when stateless is true with session store", func(t *testing.T) {
		store := session.NewMemoryStore(10 * time.Minute)
		defer func() { _ = store.Close() }()

		p := newTestPlatform(t, &platform.Config{
			Server: platform.ServerConfig{
				Name: "test",
				Streamable: platform.StreamableConfig{
					Stateless:      true,
					SessionTimeout: 10 * time.Minute,
				},
			},
			Sessions: platform.SessionsConfig{
				TTL: 10 * time.Minute,
			},
		})
		defer func() { _ = p.Close() }()

		hcfg := extractHTTPConfig(p)

		// Stateless + session store → handler should wrap.
		if !hcfg.streamableCfg.Stateless {
			t.Error("expected Stateless true")
		}

		// Verify the conditional logic would create an AwareHandler.
		if p.SessionStore() == nil {
			t.Fatal("expected non-nil session store")
		}

		inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusOK)
		})
		handler := session.NewAwareHandler(inner, session.HandlerConfig{
			Store: p.SessionStore(),
			TTL:   p.Config().Sessions.TTL,
		})

		// First request (no session) should get a session ID in response.
		req := httptest.NewRequestWithContext(context.Background(), "POST", "/", http.NoBody)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, req)

		sessionID := w.Header().Get("Mcp-Session-Id")
		if sessionID == "" {
			t.Error("expected Mcp-Session-Id header in response")
		}

		// Second request with session ID should succeed.
		req2 := httptest.NewRequestWithContext(context.Background(), "POST", "/", http.NoBody)
		req2.Header.Set("Mcp-Session-Id", sessionID)
		w2 := httptest.NewRecorder()
		handler.ServeHTTP(w2, req2)

		if w2.Code != http.StatusOK {
			t.Errorf("expected 200 for existing session, got %d", w2.Code)
		}
	})
}

// newTestPlatform creates a minimal platform for testing.
func newTestPlatform(t *testing.T, cfg *platform.Config) *platform.Platform {
	t.Helper()
	p, err := platform.New(platform.WithConfig(cfg))
	if err != nil {
		t.Fatalf("failed to create test platform: %v", err)
	}
	return p
}

// TestCloseServer_HandlesUnstartedPlatform asserts the shutdown path
// is safe when the platform was constructed but never started. This
// is the path tests and CLI sub-commands take when they only need a
// configured platform; closeServer must not panic and must complete
// in well under the K8s grace period budget.
func TestCloseServer_HandlesUnstartedPlatform(t *testing.T) {
	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{Name: "test"},
		Auth:   platform.AuthConfig{AllowAnonymous: true},
	})
	result := &serverResult{platform: p}

	done := make(chan struct{})
	go func() {
		defer close(done)
		closeServer(result)
	}()

	select {
	case <-done:
	case <-time.After(lifecycleStopTimeout + 5*time.Second):
		t.Fatal("closeServer did not return; Stop+Close must complete inside the timeout budget")
	}
}

// TestCloseServer_NilPlatformIsSafe asserts closeServer tolerates a
// serverResult whose platform field is nil (the stdio-bootstrap path
// when NewWithDefaults is used). Stop must not be invoked on a nil
// pointer and Close must be skipped.
func TestCloseServer_NilPlatformIsSafe(_ *testing.T) {
	result := &serverResult{platform: nil}
	// Must not panic.
	closeServer(result)
}

const (
	migrateTestFilePerms  = 0o600
	migrateTestInput      = "server:\n  name: test\n"
	migrateTestVersioned  = "apiVersion: v1\nserver:\n  name: test\n"
	migrateTestInputFile  = "input.yaml"
	migrateTestOutputFile = "output.yaml"
)

// writeMigrateInput creates a temp file with the given content and returns its path.
func writeMigrateInput(t *testing.T, dir, content string) string {
	t.Helper()
	p := filepath.Join(dir, migrateTestInputFile)
	if err := os.WriteFile(p, []byte(content), migrateTestFilePerms); err != nil {
		t.Fatalf("writing input: %v", err)
	}
	return p
}

// readMigrateOutput reads the output file from a migrate test.
func readMigrateOutput(t *testing.T, path string) string {
	t.Helper()
	// #nosec G304 -- test helper reading from controlled temp dir
	out, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("reading output: %v", err)
	}
	return string(out)
}

func TestRunMigrateConfig_FileToFile(t *testing.T) {
	dir := t.TempDir()
	inputPath := writeMigrateInput(t, dir, migrateTestInput)
	outputPath := filepath.Join(dir, migrateTestOutputFile)

	err := runMigrateConfig([]string{"--config", inputPath, "--output", outputPath})
	if err != nil {
		t.Fatalf("runMigrateConfig() error = %v", err)
	}

	out := readMigrateOutput(t, outputPath)
	if !strings.Contains(out, "apiVersion: v1") {
		t.Errorf("output missing apiVersion: v1, got:\n%s", out)
	}
	if !strings.Contains(out, "name: test") {
		t.Errorf("output missing original content, got:\n%s", out)
	}
}

func TestRunMigrateConfig_Idempotent(t *testing.T) {
	dir := t.TempDir()
	inputPath := writeMigrateInput(t, dir, migrateTestVersioned)
	outputPath := filepath.Join(dir, migrateTestOutputFile)

	err := runMigrateConfig([]string{"--config", inputPath, "--output", outputPath})
	if err != nil {
		t.Fatalf("runMigrateConfig() error = %v", err)
	}

	out := readMigrateOutput(t, outputPath)
	if out != migrateTestVersioned {
		t.Errorf("expected idempotent output, got:\n%s", out)
	}
}

func TestRunMigrateConfig_WithTargetVersion(t *testing.T) {
	dir := t.TempDir()
	inputPath := writeMigrateInput(t, dir, migrateTestInput)
	outputPath := filepath.Join(dir, migrateTestOutputFile)

	err := runMigrateConfig([]string{
		"--config", inputPath,
		"--output", outputPath,
		"--target-version", "v1",
	})
	if err != nil {
		t.Fatalf("runMigrateConfig() error = %v", err)
	}

	out := readMigrateOutput(t, outputPath)
	if !strings.Contains(out, "apiVersion: v1") {
		t.Errorf("output missing apiVersion: v1, got:\n%s", out)
	}
}

func TestRunMigrateConfig_MissingInputFile(t *testing.T) {
	err := runMigrateConfig([]string{"--config", "/nonexistent/path.yaml"})
	if err == nil {
		t.Fatal("expected error for missing input file")
	}
	if !strings.Contains(err.Error(), "opening config") {
		t.Errorf("error = %q, want 'opening config'", err.Error())
	}
}

func TestRunMigrateConfig_BadOutputPath(t *testing.T) {
	dir := t.TempDir()
	inputPath := writeMigrateInput(t, dir, migrateTestInput)

	err := runMigrateConfig([]string{
		"--config", inputPath,
		"--output", "/nonexistent/dir/output.yaml",
	})
	if err == nil {
		t.Fatal("expected error for bad output path")
	}
	if !strings.Contains(err.Error(), "creating output") {
		t.Errorf("error = %q, want 'creating output'", err.Error())
	}
}

func TestRunMigrateConfig_UnknownSourceVersion(t *testing.T) {
	dir := t.TempDir()
	inputPath := writeMigrateInput(t, dir, "apiVersion: v99\nserver:\n  name: test\n")

	err := runMigrateConfig([]string{"--config", inputPath})
	if err == nil {
		t.Fatal("expected error for unknown source version")
	}
	if !strings.Contains(err.Error(), "migrating config") {
		t.Errorf("error = %q, want 'migrating config'", err.Error())
	}
}

func TestMountAdminAPI(t *testing.T) {
	t.Run("skips when platform is nil", func(_ *testing.T) {
		mux := http.NewServeMux()
		mountAdminAPI(mux, nil) // should not panic
	})

	t.Run("skips when admin not enabled", func(t *testing.T) {
		p := newTestPlatform(t, &platform.Config{
			Server: platform.ServerConfig{Name: "test"},
			Admin:  platform.AdminConfig{Enabled: false},
		})
		defer func() { _ = p.Close() }()

		mux := http.NewServeMux()
		mountAdminAPI(mux, p) // should not register any routes
	})

	t.Run("mounts when admin enabled", func(t *testing.T) {
		cfg := &platform.Config{
			Server:   platform.ServerConfig{Name: "test", Transport: "http"},
			Semantic: platform.SemanticConfig{Provider: "noop"},
			Query:    platform.QueryConfig{Provider: "noop"},
			Storage:  platform.StorageConfig{Provider: "noop"},
			Admin:    platform.AdminConfig{Enabled: true, Persona: "admin"},
			Personas: platform.PersonasConfig{
				Definitions: map[string]platform.PersonaDef{
					"admin": {
						DisplayName: "Administrator",
						Roles:       []string{"admin"},
						Tools:       platform.ToolRulesDef{Allow: []string{"*"}},
					},
				},
			},
		}

		p, err := platform.New(platform.WithConfig(cfg))
		if err != nil {
			t.Fatalf("New() error = %v", err)
		}
		defer func() { _ = p.Close() }()

		mux := http.NewServeMux()
		mountAdminAPI(mux, p)

		// Admin route should be registered and return 401 (no auth)
		req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/admin/system/info", http.NoBody)
		w := httptest.NewRecorder()
		mux.ServeHTTP(w, req)

		if w.Code != http.StatusUnauthorized {
			t.Errorf("expected 401, got %d", w.Code)
		}
	})
}

// TestAdminPortalGate was removed — admin UI is now part of the unified portal
// SPA served at /portal/. Admin sections are role-gated in the SPA itself and
// the admin API routes at /api/v1/admin/ enforce server-side authorization.

// TestMountGatewayAPI covers the four branches in mountGatewayAPI:
// (1) nil platform → skip, (2) no apigateway toolkit → skip,
// (3) toolkit present + auth required → 401 on unauthenticated POST,
// (4) toolkit present + auth disabled → request reaches the handler.
// Each branch is exercised through the real ServeMux so a regression
// in the registration path is caught here, not at runtime.
func TestMountGatewayAPI(t *testing.T) {
	t.Run("skips when platform is nil", func(_ *testing.T) {
		mux := http.NewServeMux()
		server := mcp.NewServer(&mcp.Implementation{Name: "t", Version: "v1"}, nil)
		mountGatewayAPI(mux, server, nil, false)
	})

	t.Run("skips when no apigateway toolkit is loaded", func(t *testing.T) {
		p := newTestPlatform(t, &platform.Config{
			Server:   platform.ServerConfig{Name: "test"},
			Semantic: platform.SemanticConfig{Provider: "noop"},
			Query:    platform.QueryConfig{Provider: "noop"},
			Storage:  platform.StorageConfig{Provider: "noop"},
		})
		defer func() { _ = p.Close() }()

		mux := http.NewServeMux()
		server := mcp.NewServer(&mcp.Implementation{Name: "t", Version: "v1"}, nil)
		mountGatewayAPI(mux, server, p, false)

		// No /api/v1/gateway/ route should have been registered.
		req := httptest.NewRequestWithContext(context.Background(), http.MethodPost,
			"/api/v1/gateway/acme/invoke", strings.NewReader(`{}`))
		w := httptest.NewRecorder()
		mux.ServeHTTP(w, req)
		if w.Code != http.StatusNotFound {
			t.Errorf("expected 404 (no route), got %d", w.Code)
		}
	})

	t.Run("mounts with auth wrapper when requireAuth is true", func(t *testing.T) {
		p := newGatewayTestPlatform(t)
		defer func() { _ = p.Close() }()

		mux := http.NewServeMux()
		server := mcp.NewServer(&mcp.Implementation{Name: "t", Version: "v1"}, nil)
		mountGatewayAPI(mux, server, p, true)

		// Unauthenticated request must be rejected before reaching the
		// handler. The auth wrapper returns 401 with a missing-token
		// message — that's what proves requireAuth=true wired through.
		req := httptest.NewRequestWithContext(context.Background(), http.MethodPost,
			"/api/v1/gateway/acme/invoke", strings.NewReader(`{"method":"GET","path":"/x"}`))
		w := httptest.NewRecorder()
		mux.ServeHTTP(w, req)
		if w.Code != http.StatusUnauthorized {
			t.Errorf("expected 401 from auth wrapper, got %d", w.Code)
		}
	})

	t.Run("mounts without auth wrapper when requireAuth is false", func(t *testing.T) {
		p := newGatewayTestPlatform(t)
		defer func() { _ = p.Close() }()

		mux := http.NewServeMux()
		server := mcp.NewServer(&mcp.Implementation{Name: "t", Version: "v1"}, nil)
		mountGatewayAPI(mux, server, p, false)

		// Anonymous request reaches the handler; without the
		// api_invoke_endpoint tool registered on this bare MCP server,
		// the in-memory CallTool returns an error → 500.
		req := httptest.NewRequestWithContext(context.Background(), http.MethodPost,
			"/api/v1/gateway/acme/invoke", strings.NewReader(`{"method":"GET","path":"/x"}`))
		w := httptest.NewRecorder()
		mux.ServeHTTP(w, req)
		if w.Code == http.StatusUnauthorized {
			t.Errorf("expected handler to be reached without auth wrapper, but got 401")
		}
	})
}

// newGatewayTestPlatform builds a minimal platform with an apigateway
// toolkit loaded. The toolkit needs at least one configured kind in
// `toolkits.api` for the registry GetByKind(api) lookup to return a
// non-empty slice, which is what mountGatewayAPI checks before
// registering the REST route.
func newGatewayTestPlatform(t *testing.T) *platform.Platform {
	t.Helper()
	return newTestPlatform(t, &platform.Config{
		Server:   platform.ServerConfig{Name: "test"},
		Semantic: platform.SemanticConfig{Provider: "noop"},
		Query:    platform.QueryConfig{Provider: "noop"},
		Storage:  platform.StorageConfig{Provider: "noop"},
		Toolkits: map[string]any{
			"api": map[string]any{
				"enabled": true,
				"instances": map[string]any{
					"acme": map[string]any{
						"base_url":  "https://api.example.com",
						"auth_mode": "none",
					},
				},
			},
		},
	})
}

func TestBuildAdminHandler(t *testing.T) {
	cfg := &platform.Config{
		Server: platform.ServerConfig{
			Name:      "test-platform",
			Transport: "http",
		},
		Semantic: platform.SemanticConfig{Provider: "noop"},
		Query:    platform.QueryConfig{Provider: "noop"},
		Storage:  platform.StorageConfig{Provider: "noop"},
		Admin: platform.AdminConfig{
			Enabled: true,
			Persona: "admin",
		},
		Auth: platform.AuthConfig{
			APIKeys: platform.APIKeyAuthConfig{
				Enabled: true,
				Keys: []platform.APIKeyDef{
					{Key: "test-admin-key", Name: "admin", Roles: []string{"admin"}},
				},
			},
		},
		Personas: platform.PersonasConfig{
			Definitions: map[string]platform.PersonaDef{
				"admin": {
					DisplayName: "Administrator",
					Roles:       []string{"admin"},
					Tools:       platform.ToolRulesDef{Allow: []string{"*"}},
				},
			},
		},
	}

	p, err := platform.New(platform.WithConfig(cfg))
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	defer func() { _ = p.Close() }()

	handler := buildAdminHandler(p)
	if handler == nil {
		t.Fatal("buildAdminHandler() returned nil")
	}

	// The handler should respond to admin routes
	req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/admin/system/info", http.NoBody)
	req.Header.Set("X-API-Key", "test-key")
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

	// Without a valid API key, we expect 401
	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected 401 for unauthenticated request, got %d", w.Code)
	}
}

func TestBuildRootHandler_NilPlatform(t *testing.T) {
	mcpServer := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)
	handler := buildRootHandler(mcpServer, nil, httpConfig{})
	if handler == nil {
		t.Fatal("expected non-nil handler")
	}
}

func TestBuildRootHandler_WithSessionStore(t *testing.T) {
	mcpServer := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)

	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{
			Name: "test",
			Streamable: platform.StreamableConfig{
				Stateless:      true,
				SessionTimeout: testSessionTimeout,
			},
		},
		Sessions: platform.SessionsConfig{
			TTL: testSessionTimeout,
		},
	})
	defer func() { _ = p.Close() }()

	hcfg := extractHTTPConfig(p)
	handler := buildRootHandler(mcpServer, p, hcfg)
	if handler == nil {
		t.Fatal("expected non-nil handler")
	}

	// The handler should be wrapped with session awareness
	req := httptest.NewRequestWithContext(context.Background(), "POST", "/", http.NoBody)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

	// Session-aware handler adds Mcp-Session-Id header
	if sessionID := w.Header().Get("Mcp-Session-Id"); sessionID == "" {
		t.Error("expected Mcp-Session-Id header from session-aware wrapper")
	}
}

func TestMountRootHandler_AuthRequired(t *testing.T) {
	mux := http.NewServeMux()
	inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	mountRootHandler(mux, inner, httpConfig{requireAuth: true}, "")

	req := httptest.NewRequestWithContext(context.Background(), "POST", "/", http.NoBody)
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)

	// Auth gateway should reject unauthenticated requests
	if w.Code == http.StatusOK {
		t.Error("expected auth gateway to reject request")
	}
}

func TestMountRootHandler_NoAuth(t *testing.T) {
	mux := http.NewServeMux()
	inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	mountRootHandler(mux, inner, httpConfig{requireAuth: false}, "")

	req := httptest.NewRequestWithContext(context.Background(), "POST", "/", http.NoBody)
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("expected 200, got %d", w.Code)
	}
}

func TestBrowserRedirectMiddleware(t *testing.T) {
	inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	handler := browserRedirectMiddleware(inner)

	t.Run("redirects browsers", func(t *testing.T) {
		req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
		req.Header.Set("Accept", "text/html,application/xhtml+xml")
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, req)

		if w.Code != http.StatusTemporaryRedirect {
			t.Errorf("expected 307, got %d", w.Code)
		}
		if loc := w.Header().Get("Location"); loc != "/portal/" {
			t.Errorf("Location = %q, want /portal/", loc)
		}
	})

	t.Run("passes through non-browser", func(t *testing.T) {
		req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/", http.NoBody)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, req)

		if w.Code != http.StatusOK {
			t.Errorf("expected 200, got %d", w.Code)
		}
	})

	t.Run("passes through GET without html accept", func(t *testing.T) {
		req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
		req.Header.Set("Accept", "application/json")
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, req)

		if w.Code != http.StatusOK {
			t.Errorf("expected 200, got %d", w.Code)
		}
	})
}

func TestMountRootHandler_AuthWithPortalUI_BrowserRedirects(t *testing.T) {
	mux := http.NewServeMux()
	inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	mountRootHandler(mux, inner, httpConfig{requireAuth: true, portalUI: true}, "")

	// Browser request should redirect to /portal/ instead of getting 401.
	req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept", "text/html,application/xhtml+xml")
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)

	if w.Code != http.StatusTemporaryRedirect {
		t.Errorf("browser request: expected 307 redirect, got %d", w.Code)
	}
	if loc := w.Header().Get("Location"); loc != "/portal/" {
		t.Errorf("Location = %q, want /portal/", loc)
	}
}

func TestMountRootHandler_AuthWithPortalUI_MCPStillRequiresAuth(t *testing.T) {
	mux := http.NewServeMux()
	inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	mountRootHandler(mux, inner, httpConfig{requireAuth: true, portalUI: true}, "")

	// MCP request (POST, no Accept: text/html) should still hit auth gateway.
	req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/", http.NoBody)
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, req)

	if w.Code == http.StatusOK {
		t.Error("MCP request should still be rejected by auth gateway")
	}
}

func TestMountBrowserAuth_NilPlatform(_ *testing.T) {
	mux := http.NewServeMux()
	mountBrowserAuth(mux, nil) // should not panic
}

func TestMountBrowserAuth_NilFlow(t *testing.T) {
	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{Name: "test"},
	})
	defer func() { _ = p.Close() }()

	mux := http.NewServeMux()
	mountBrowserAuth(mux, p) // should not register routes (no browser session)
}

func TestMountPortalUI_Disabled(t *testing.T) {
	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{Name: "test"},
		Portal: platform.PortalConfig{Enabled: new(false)},
	})
	defer func() { _ = p.Close() }()

	mux := http.NewServeMux()
	mountPortalUI(mux, p, true) // UI disabled in config
}

func TestMountPortalUI_NoAssets(t *testing.T) {
	p := newTestPlatform(t, &platform.Config{
		Server: platform.ServerConfig{Name: "test"},
		Portal: platform.PortalConfig{Enabled: new(true)},
	})
	defer func() { _ = p.Close() }()

	mux := http.NewServeMux()
	mountPortalUI(mux, p, false) // no assets available
}

func TestMcpappsBrandName(t *testing.T) {
	t.Run("returns brand_name from mcpapps config", func(t *testing.T) {
		p := newTestPlatform(t, &platform.Config{
			Server: platform.ServerConfig{Name: "test"},
			MCPApps: platform.MCPAppsConfig{
				Apps: map[string]platform.AppConfig{
					"platform-info": {
						Config: map[string]any{"brand_name": "Plexara"},
					},
				},
			},
		})
		defer func() { _ = p.Close() }()

		got := mcpappsBrandName(p)
		if got != "Plexara" {
			t.Errorf("mcpappsBrandName() = %q, want %q", got, "Plexara")
		}
	})

	t.Run("returns empty when no platform-info app", func(t *testing.T) {
		p := newTestPlatform(t, &platform.Config{
			Server: platform.ServerConfig{Name: "test"},
		})
		defer func() { _ = p.Close() }()

		got := mcpappsBrandName(p)
		if got != "" {
			t.Errorf("mcpappsBrandName() = %q, want empty", got)
		}
	})

	t.Run("returns empty when brand_name not in config", func(t *testing.T) {
		p := newTestPlatform(t, &platform.Config{
			Server: platform.ServerConfig{Name: "test"},
			MCPApps: platform.MCPAppsConfig{
				Apps: map[string]platform.AppConfig{
					"platform-info": {
						Config: map[string]any{"other_key": "value"},
					},
				},
			},
		})
		defer func() { _ = p.Close() }()

		got := mcpappsBrandName(p)
		if got != "" {
			t.Errorf("mcpappsBrandName() = %q, want empty", got)
		}
	})
}

func TestBuildResourceClaims(t *testing.T) {
	reg := persona.NewRegistry()
	_ = reg.Register(&persona.Persona{
		Name:  "admin",
		Roles: []string{"dp_admin"},
	})
	_ = reg.Register(&persona.Persona{
		Name:  "analyst",
		Roles: []string{"dp_analyst"},
	})

	t.Run("prefixed admin role resolves IsAdmin via persona", func(t *testing.T) {
		user := &portal.User{
			UserID: "u1",
			Email:  "admin@example.com",
			Roles:  []string{"dp_admin"},
		}
		claims := buildResourceClaims(user, reg, "admin")
		if !claims.IsAdmin {
			t.Error("expected IsAdmin=true for user with dp_admin role mapped to admin persona")
		}
		if !slices.Contains(claims.Personas, "admin") {
			t.Errorf("expected admin in Personas, got %v", claims.Personas)
		}
	})

	t.Run("non-admin role does not set IsAdmin", func(t *testing.T) {
		user := &portal.User{
			UserID: "u2",
			Email:  "analyst@example.com",
			Roles:  []string{"dp_analyst"},
		}
		claims := buildResourceClaims(user, reg, "admin")
		if claims.IsAdmin {
			t.Error("expected IsAdmin=false for non-admin user")
		}
		if !slices.Contains(claims.Personas, "analyst") {
			t.Errorf("expected analyst in Personas, got %v", claims.Personas)
		}
	})

	t.Run("nil registry skips persona resolution", func(t *testing.T) {
		user := &portal.User{
			UserID: "u3",
			Email:  "u3@example.com",
			Roles:  []string{"dp_admin"},
		}
		claims := buildResourceClaims(user, nil, "admin")
		if claims.IsAdmin {
			t.Error("expected IsAdmin=false when registry is nil")
		}
		if len(claims.Personas) != 0 {
			t.Errorf("expected no personas, got %v", claims.Personas)
		}
	})

	t.Run("user with multiple roles gets all matching personas", func(t *testing.T) {
		user := &portal.User{
			UserID: "u4",
			Email:  "multi@example.com",
			Roles:  []string{"dp_admin", "dp_analyst"},
		}
		claims := buildResourceClaims(user, reg, "admin")
		if !claims.IsAdmin {
			t.Error("expected IsAdmin=true")
		}
		if len(claims.Personas) != 2 {
			t.Errorf("expected 2 personas, got %v", claims.Personas)
		}
	})

	t.Run("persona-admin roles populate AdminOfPersonas", func(t *testing.T) {
		user := &portal.User{
			UserID: "u5",
			Email:  "pa@example.com",
			Roles:  []string{"dp_persona-admin:finance", "dp_analyst"},
		}
		claims := buildResourceClaims(user, reg, "admin")
		if !slices.Contains(claims.AdminOfPersonas, "finance") {
			t.Errorf("expected finance in AdminOfPersonas, got %v", claims.AdminOfPersonas)
		}
	})
}

func TestExtractPersonaAdminRoles(t *testing.T) {
	t.Run("unprefixed role", func(t *testing.T) {
		got := extractPersonaAdminRoles([]string{"persona-admin:finance"})
		if len(got) != 1 || got[0] != "finance" {
			t.Errorf("got %v, want [finance]", got)
		}
	})

	t.Run("prefixed role", func(t *testing.T) {
		got := extractPersonaAdminRoles([]string{"dp_persona-admin:engineering"})
		if len(got) != 1 || got[0] != "engineering" {
			t.Errorf("got %v, want [engineering]", got)
		}
	})

	t.Run("multiple persona-admin roles", func(t *testing.T) {
		got := extractPersonaAdminRoles([]string{"dp_persona-admin:finance", "dp_persona-admin:ops", "dp_analyst"})
		if len(got) != 2 {
			t.Errorf("got %v, want 2 entries", got)
		}
	})

	t.Run("no persona-admin roles", func(t *testing.T) {
		got := extractPersonaAdminRoles([]string{"dp_admin", "dp_analyst"})
		if len(got) != 0 {
			t.Errorf("got %v, want empty", got)
		}
	})

	t.Run("empty persona name ignored", func(t *testing.T) {
		got := extractPersonaAdminRoles([]string{"persona-admin:"})
		if len(got) != 0 {
			t.Errorf("got %v, want empty for trailing colon", got)
		}
	})
}
