package router_test

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"github.com/stretchr/testify/require"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
	"go.opentelemetry.io/otel/trace/embedded"

	"github.com/tidbcloud/AgentTeam/internal/api"
	adminapi "github.com/tidbcloud/AgentTeam/internal/api/admin"
	"github.com/tidbcloud/AgentTeam/internal/api/agent"
	"github.com/tidbcloud/AgentTeam/internal/api/im"
	"github.com/tidbcloud/AgentTeam/internal/api/llm"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/binding"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/invocation"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/plan"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/run"
	"github.com/tidbcloud/AgentTeam/internal/apr/runtime"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/observability/logging"
	"github.com/tidbcloud/AgentTeam/internal/observability/metrics"
	"github.com/tidbcloud/AgentTeam/internal/observability/tracing"
	"github.com/tidbcloud/AgentTeam/internal/router"
	"github.com/tidbcloud/AgentTeam/internal/store/admin"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/workflow/bindingmanage"
	"github.com/tidbcloud/AgentTeam/internal/workflow/calendar"
	"github.com/tidbcloud/AgentTeam/internal/workflow/invocationmanage"
	"github.com/tidbcloud/AgentTeam/internal/workflow/orchestrator"
	"github.com/tidbcloud/AgentTeam/internal/workflow/planmanage"
	"github.com/tidbcloud/AgentTeam/internal/workflow/runmanage"
)

type routeWorkflowRunCallbackRuntime struct{}

func (routeWorkflowRunCallbackRuntime) ExecuteCallback(context.Context, orchestrator.CallbackCommand) (orchestrator.CallbackResult, error) {
	return orchestrator.CallbackResult{}, nil
}

func (routeWorkflowRunCallbackRuntime) ListCallbacks(context.Context, orchestrator.CallbackListCommand) (orchestrator.CallbackListResult, error) {
	return orchestrator.CallbackListResult{Steps: []orchestrator.CallbackResult{}}, nil
}

type routeAgentAuthenticator struct{}

func (routeAgentAuthenticator) AuthenticateAgent(context.Context, string, string) (storeagent.AuthenticatedAgent, error) {
	return storeagent.AuthenticatedAgent{}, nil
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	router := router.NewRouter(router.Dependencies{Logger: logger})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/this-route-does-not-exist", nil)
	router.ServeHTTP(response, request)

	require.Equal(t, http.StatusNotFound, response.Code)
	require.Contains(t, response.Header().Get("Content-Type"), "application/json")

	var payload map[string]string
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &payload))
	require.Equal(t, "not found", payload["error"])
}

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

	distDir := t.TempDir()
	require.NoError(t, os.Mkdir(filepath.Join(distDir, "assets"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(distDir, "index.html"), []byte("<html>agentteam app</html>"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(distDir, "assets", "app.js"), []byte("console.log('agentteam')"), 0o644))

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	httpRouter := router.NewRouter(router.Dependencies{
		Logger: logger,
		Web: router.WebDependencies{
			ServeWeb: true,
			DistDir:  distDir,
		},
	})

	assetResponse := httptest.NewRecorder()
	httpRouter.ServeHTTP(assetResponse, httptest.NewRequest(http.MethodGet, "/assets/app.js", nil))
	require.Equal(t, http.StatusOK, assetResponse.Code)
	require.Equal(t, "console.log('agentteam')", assetResponse.Body.String())
	require.Equal(t, "public, max-age=31536000, immutable", assetResponse.Header().Get("Cache-Control"))

	missingAssetResponse := httptest.NewRecorder()
	httpRouter.ServeHTTP(missingAssetResponse, httptest.NewRequest(http.MethodGet, "/assets/missing.js", nil))
	require.Equal(t, http.StatusNotFound, missingAssetResponse.Code)
	require.NotContains(t, missingAssetResponse.Body.String(), "agentteam app")

	fallbackResponse := httptest.NewRecorder()
	httpRouter.ServeHTTP(fallbackResponse, httptest.NewRequest(http.MethodGet, "/channels/general", nil))
	require.Equal(t, http.StatusOK, fallbackResponse.Code)
	require.Contains(t, fallbackResponse.Body.String(), "agentteam app")
	require.Equal(t, "no-cache", fallbackResponse.Header().Get("Cache-Control"))
}

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

	distDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(distDir, "index.html"), []byte("<html>agentteam app</html>"), 0o644))

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	httpRouter := router.NewRouter(router.Dependencies{
		Logger: logger,
		Web: router.WebDependencies{
			ServeWeb: true,
			DistDir:  distDir,
		},
	})

	reservedPaths := []string{
		"/api/not-registered",
		"/internal/not-registered",
		"/readyz/not-registered",
	}
	for _, path := range reservedPaths {
		response := httptest.NewRecorder()
		httpRouter.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
		require.Equal(t, http.StatusNotFound, response.Code, path)
		require.Contains(t, response.Header().Get("Content-Type"), "application/json", path)
		require.NotContains(t, response.Body.String(), "agentteam app", path)
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	router := router.NewRouter(router.Dependencies{Logger: logger})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/healthz", nil)
	router.ServeHTTP(response, request)

	require.Equal(t, http.StatusMethodNotAllowed, response.Code)
	require.Contains(t, response.Header().Get("Content-Type"), "application/json")

	var payload map[string]string
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &payload))
	require.Equal(t, "method not allowed", payload["error"])
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	router := router.NewRouter(router.Dependencies{Logger: logger})
	router.GET("/panic", func(ctx *gin.Context) {
		panic("boom")
	})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/panic", nil)
	router.ServeHTTP(response, request)

	require.Equal(t, http.StatusInternalServerError, response.Code)
	require.Contains(t, response.Header().Get("Content-Type"), "application/json")

	var payload map[string]string
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &payload))
	require.Equal(t, "internal server error", payload["error"])
}

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

	logger, logs := observedRouterLogger()
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/public/config", nil)
	request.Header.Set("X-Request-ID", "abc-123")
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "abc-123", response.Header().Get("X-Request-ID"))

	entry := loggedEntry(t, logs)
	require.Equal(t, "abc-123", entry["request_id"])
	require.Equal(t, "http request", entry["msg"])
}

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

	logger, logs := observedRouterLogger()
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/daemon/connect?key=machine-secret&accessToken=browser-secret&token=plain-secret", nil)
	httpRouter.ServeHTTP(response, request)

	entry := loggedEntry(t, logs)
	require.Equal(t, "/api/v1/workspaces/wsp_1/daemon/connect", entry["path"])
	require.NotContains(t, entry, "query")
	require.NotContains(t, logs.String(), "machine-secret")
	require.NotContains(t, logs.String(), "browser-secret")
	require.NotContains(t, logs.String(), "plain-secret")
}

// TestNewRouter_RequestLogAndSpanOmitDirectoryBodyPathMarker pins the
// router-level path-marker invariant for endpoints such as
// /api/v1/workspaces/:workspaceId/machines/:machineId/directories:list
// where the user-supplied directory path travels in the JSON request body.
// The request log and the trace span attributes capture the URL path
// (workspace/machine IDs only) but MUST NEVER echo the body path marker,
// even when the handler reads the request body end-to-end. A synthetic
// route is used because the production directories:list route is already
// registered on NewRouter; the router middleware invariant is identical
// for any POST route that carries sensitive payload in the body.
func TestNewRouter_RequestLogAndSpanOmitDirectoryBodyPathMarker(t *testing.T) {
	t.Parallel()

	const marker = "SECRET_PATH_MARKER_xyz"
	logger, logs := observedRouterLogger()
	httpTracing, recorder := newRecordingHTTPTracing()
	httpRouter := router.NewRouter(router.Dependencies{
		Logger:      logger,
		HTTPTracing: httpTracing,
	})

	// Mount a stub POST route that mirrors the production
	// directories:list shape: opaque IDs in the URL, user-supplied path
	// in the JSON body. The handler drains the body so the marker is
	// exercised through the request lifecycle.
	httpRouter.POST(
		"/api/v1/workspaces/:workspaceId/machines/:machineId/directory-browse-test",
		func(ctx *gin.Context) {
			_, _ = io.Copy(io.Discard, ctx.Request.Body)
			ctx.Status(http.StatusOK)
		},
	)

	body := strings.NewReader(`{"path":"/users/` + marker + `"}`)
	request := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/workspaces/wsp_1/machines/mach_1/directory-browse-test",
		body,
	)
	request.Header.Set("Content-Type", "application/json")
	response := httptest.NewRecorder()
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)

	// Request-log invariant: the path field carries URL.Path (IDs only),
	// and the marker must not appear anywhere in the serialized log stream.
	entry := loggedEntry(t, logs)
	require.Equal(
		t,
		"/api/v1/workspaces/wsp_1/machines/mach_1/directory-browse-test",
		entry["path"],
	)
	require.NotContains(t, logs.String(), marker,
		"request log MUST NOT leak the body path marker")

	// Trace-span invariant: span name and every recorded attribute are
	// bound to the URL surface only. Body content must never appear.
	spans := recorder.endedSpans()
	require.Len(t, spans, 1)
	span := spans[0]
	require.NotContains(t, span.name, marker,
		"span name MUST NOT leak the body path marker")
	for _, attr := range span.attrs {
		require.NotContainsf(
			t,
			attr.Value.AsString(),
			marker,
			"span attribute %q MUST NOT leak the body path marker",
			string(attr.Key),
		)
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/public/config", nil)
	request.Header.Set("X-Request-ID", "bad request id")
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	generated := response.Header().Get("X-Request-ID")
	require.NotEmpty(t, generated)
	require.NotEqual(t, "bad request id", generated)
	_, err := uuid.Parse(generated)
	require.NoError(t, err)
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	reg := prometheus.NewRegistry()
	httpMetrics, err := metrics.RegisterHTTP(reg)
	require.NoError(t, err)
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger, HTTPMetrics: httpMetrics})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/healthz", nil)
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, 0, metricCount(t, reg, "agentteam_http_requests_total"))
	require.Equal(t, 0, metricCount(t, reg, "agentteam_http_request_duration_seconds"))
	require.Equal(t, float64(0), metricValue(t, reg, "agentteam_http_inflight", nil))
}

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

	logger, logs := observedRouterLogger()
	reg := prometheus.NewRegistry()
	httpMetrics, err := metrics.RegisterHTTP(reg)
	require.NoError(t, err)
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger, HTTPMetrics: httpMetrics})
	httpRouter.GET("/panic", func(ctx *gin.Context) {
		panic("boom")
	})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/panic", nil)
	request.Header.Set("X-Request-ID", "panic-req")
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusInternalServerError, response.Code)
	require.Equal(t, float64(1), metricValue(t, reg, "agentteam_http_panics_total", map[string]string{"method": http.MethodGet, "route": "/panic"}))
	require.Equal(t, float64(1), metricValue(t, reg, "agentteam_http_requests_total", map[string]string{"method": http.MethodGet, "route": "/panic", "status": "500"}))

	entries := loggedEntries(t, logs)
	require.Len(t, entries, 2)
	require.Equal(t, "panic-req", entries[0]["request_id"])
	require.Equal(t, "http request", entries[0]["msg"])
	require.Equal(t, float64(http.StatusInternalServerError), entries[0]["status"])
	require.Equal(t, "panic-req", entries[1]["request_id"])
	require.Equal(t, "http request panic recovered", entries[1]["msg"])
}

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

	logger, logs := observedRouterLogger()
	httpTracing, recorder := newRecordingHTTPTracing()
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger, HTTPTracing: httpTracing})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/public/config", nil)
	request.Header.Set("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	spans := recorder.endedSpans()
	require.Len(t, spans, 1)
	span := spans[0]
	require.Equal(t, "HTTP GET /api/v1/public/config", span.name)
	require.Equal(t, trace.SpanKindServer, span.kind)
	require.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", span.parent.TraceID().String())
	require.Equal(t, "00f067aa0ba902b7", span.parent.SpanID().String())
	require.Equal(t, "GET", spanStringAttribute(t, span, "http.request.method"))
	require.Equal(t, "/api/v1/public/config", spanStringAttribute(t, span, "http.route"))
	require.Equal(t, int64(http.StatusOK), spanIntAttribute(t, span, "http.response.status_code"))

	entry := loggedEntry(t, logs)
	require.Equal(t, "http request", entry["msg"])
	require.Equal(t, span.spanContext.TraceID().String(), entry["trace_id"])
	require.Equal(t, span.spanContext.SpanID().String(), entry["span_id"])
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	httpTracing, recorder := newRecordingHTTPTracing()
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger, HTTPTracing: httpTracing})

	notFoundResponse := httptest.NewRecorder()
	httpRouter.ServeHTTP(notFoundResponse, httptest.NewRequest(http.MethodGet, "/missing/path", nil))
	require.Equal(t, http.StatusNotFound, notFoundResponse.Code)

	methodResponse := httptest.NewRecorder()
	httpRouter.ServeHTTP(methodResponse, httptest.NewRequest(http.MethodPost, "/healthz", nil))
	require.Equal(t, http.StatusMethodNotAllowed, methodResponse.Code)

	spans := recorder.endedSpans()
	require.Len(t, spans, 2)
	require.Equal(t, "HTTP GET <not_found>", spans[0].name)
	require.Equal(t, "HTTP POST <method_not_allowed>", spans[1].name)
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	reg := prometheus.NewRegistry()
	httpMetrics, err := metrics.RegisterHTTP(reg)
	require.NoError(t, err)
	httpTracing, recorder := newRecordingHTTPTracing()
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger, HTTPMetrics: httpMetrics, HTTPTracing: httpTracing})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/healthz", nil)
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, 0, metricCount(t, reg, "agentteam_http_requests_total"))
	spans := recorder.endedSpans()
	require.Len(t, spans, 1)
	require.Equal(t, "HTTP GET /healthz", spans[0].name)
}

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

	logger, logs := observedRouterLogger()
	httpTracing, recorder := newRecordingHTTPTracing()
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger, HTTPTracing: httpTracing})
	httpRouter.GET("/panic", func(ctx *gin.Context) {
		panic("boom")
	})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/panic", nil)
	request.Header.Set("X-Request-ID", "trace-panic")
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusInternalServerError, response.Code)
	spans := recorder.endedSpans()
	require.Len(t, spans, 1)
	span := spans[0]
	require.Equal(t, "HTTP GET /panic", span.name)
	require.Equal(t, codes.Error, span.status)
	require.Equal(t, int64(http.StatusInternalServerError), spanIntAttribute(t, span, "http.response.status_code"))
	require.Len(t, span.errors, 1)

	entries := loggedEntries(t, logs)
	require.Len(t, entries, 2)
	require.Equal(t, "trace-panic", entries[0]["request_id"])
	require.Equal(t, span.spanContext.TraceID().String(), entries[0]["trace_id"])
	require.Equal(t, span.spanContext.SpanID().String(), entries[0]["span_id"])
	require.Equal(t, "trace-panic", entries[1]["request_id"])
	require.Equal(t, span.spanContext.TraceID().String(), entries[1]["trace_id"])
	require.Equal(t, span.spanContext.SpanID().String(), entries[1]["span_id"])
}

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

	handlerErr := errors.New("database exploded")
	logger, logs := observedRouterLogger()
	httpTracing, recorder := newRecordingHTTPTracing()
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger, HTTPTracing: httpTracing})
	httpRouter.GET("/handler-error", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, api.HandleAuth(func(api.AuthenticatedRequest) (api.Response, error) {
		return api.Response{}, handlerErr
	}))

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/handler-error", nil)
	request.Header.Set("X-Request-ID", "handler-error-req")
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusInternalServerError, response.Code)

	spans := recorder.endedSpans()
	require.Len(t, spans, 1)
	span := spans[0]
	require.Equal(t, "HTTP GET /handler-error", span.name)
	require.Equal(t, codes.Error, span.status)
	require.Len(t, span.errors, 1)
	require.Same(t, handlerErr, span.errors[0])

	entries := loggedEntries(t, logs)
	require.Len(t, entries, 2)
	require.Equal(t, "http request", entries[0]["msg"])
	require.Equal(t, "handler-error-req", entries[0]["request_id"])
	require.Equal(t, span.spanContext.TraceID().String(), entries[0]["trace_id"])
	require.Equal(t, span.spanContext.SpanID().String(), entries[0]["span_id"])
	require.Equal(t, float64(http.StatusInternalServerError), entries[0]["status"])
	require.Equal(t, "http request failed", entries[1]["msg"])
	require.Equal(t, "handler-error-req", entries[1]["request_id"])
	require.Equal(t, span.spanContext.TraceID().String(), entries[1]["trace_id"])
	require.Equal(t, span.spanContext.SpanID().String(), entries[1]["span_id"])
	require.Equal(t, float64(http.StatusInternalServerError), entries[1]["status"])
	require.Equal(t, handlerErr.Error(), entries[1]["error"])
}

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

	logger, logs := observedRouterLogger()
	httpTracing, recorder := newRecordingHTTPTracing()
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger, HTTPTracing: httpTracing})
	httpRouter.GET("/handler-client-error", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, api.HandleAuth(func(api.AuthenticatedRequest) (api.Response, error) {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "bad request")
	}))

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/handler-client-error", nil)
	request.Header.Set("X-Request-ID", "handler-client-error-req")
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusBadRequest, response.Code)
	spans := recorder.endedSpans()
	require.Len(t, spans, 1)
	require.Empty(t, spans[0].errors)

	entries := loggedEntries(t, logs)
	require.Len(t, entries, 1)
	require.Equal(t, "http request", entries[0]["msg"])
	require.Equal(t, "handler-client-error-req", entries[0]["request_id"])
	require.Equal(t, float64(http.StatusBadRequest), entries[0]["status"])
}

func TestNewRouter_PprofRoutesRequireEnvAndLocalhost(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))

	t.Run("disabled by default", func(t *testing.T) {
		t.Setenv("AGENTTEAM_ENABLE_PPROF", "")
		httpRouter := router.NewRouter(router.Dependencies{Logger: logger})

		response := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodGet, "/debug/pprof", nil)
		request.RemoteAddr = "127.0.0.1:12345"
		httpRouter.ServeHTTP(response, request)

		require.Equal(t, http.StatusNotFound, response.Code)
	})

	t.Run("enabled for localhost", func(t *testing.T) {
		t.Setenv("AGENTTEAM_ENABLE_PPROF", "1")
		httpRouter := router.NewRouter(router.Dependencies{Logger: logger})

		response := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodGet, "/debug/pprof/", nil)
		request.RemoteAddr = "127.0.0.1:12345"
		httpRouter.ServeHTTP(response, request)

		require.Equal(t, http.StatusOK, response.Code)
		require.Contains(t, response.Body.String(), "Types of profiles available")
	})

	t.Run("rejects non-localhost", func(t *testing.T) {
		t.Setenv("AGENTTEAM_ENABLE_PPROF", "1")
		httpRouter := router.NewRouter(router.Dependencies{Logger: logger})

		response := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodGet, "/debug/pprof/", nil)
		request.RemoteAddr = "192.0.2.10:12345"
		httpRouter.ServeHTTP(response, request)

		require.Equal(t, http.StatusForbidden, response.Code)
	})
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	httpRouter := router.NewRouter(router.Dependencies{Logger: logger})
	counts := routeCounts(httpRouter)

	for _, route := range migratedDomainRoutes() {
		require.Equal(t, 1, counts[route], route)
	}
	for _, route := range organizationDomainRoutes() {
		require.Equal(t, 1, counts[route], route)
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	httpRouter := router.NewRouterForProfile(router.ProfileCentral, router.Dependencies{Logger: logger})
	counts := routeCounts(httpRouter)

	for _, route := range []string{
		"GET /healthz",
		"GET /readyz",
		"GET /api/avatars/:avatarId",
		"GET /api/v1/public/config",
		"POST /api/v1/auth/login",
		"POST /api/v1/machine-connect-exchanges",
		"POST /api/v1/machine-auth-resolutions",
		"GET /api/v1/me/model-profiles",
		"POST /api/v1/me/model-profiles:action",
	} {
		require.Equal(t, 1, counts[route], route)
	}
	for _, route := range []string{
		"GET /api/v1/kb",
		"GET /api/v1/kb/*forwardPath",
		"GET /api/v1/skills/*forwardPath",
		"GET /api/v1/mcp/*forwardPath",
		"GET /api/v1/repositories/*forwardPath",
		"GET /api/v1/automations/*forwardPath",
		"GET /api/v3/*forwardPath",
		"GET /api/graphql",
		"GET /api/v1/workspaces/:workspaceId/providers",
		"GET /api/v1/workspaces/:workspaceId/selectable-models",
		"GET /api/v1/workspaces/:workspaceId/sidebar/channel-sections",
		"PUT /api/v1/workspaces/:workspaceId/sidebar/channel-sections",
		"GET /api/v1/workspaces/:workspaceId/channels",
		"GET /api/v1/workspaces/:workspaceId/tasks/filter-options",
		"GET /api/v1/workspaces/:workspaceId/task-board/options",
		"GET /api/v1/workspaces/:workspaceId/machines",
		"GET /api/v1/workspaces/:workspaceId/schedules",
		"GET /api/v1/workspaces/:workspaceId/triggers",
		"GET /api/v1/workspaces/:workspaceId/apps",
		"GET /api/v1/workspaces/:workspaceId/app-installations/:installationId/service-event-subscriptions",
		"POST /api/apps/github/webhook",
		"POST /api/apps/gitea/webhook",
		"GET /api/v1/workspaces/:workspaceId/agents",
		"GET /internal/admin/public-repository-caches",
	} {
		require.Zero(t, counts[route], route)
	}
	for _, route := range []string{
		"GET /api/v1/workspaces/:workspaceId/providers",
		"GET /api/v1/workspaces/:workspaceId/selectable-models",
		"GET /api/llm/healthz",
	} {
		require.Zero(t, counts[route], route)
	}
}

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

	seenPaths := make(chan string, 2)
	meshXAdmin := http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		seenPaths <- request.URL.Path
		if request.Header.Get("Authorization") != "Bearer meshx-admin-token" {
			response.WriteHeader(http.StatusUnauthorized)
			return
		}
		response.WriteHeader(http.StatusNoContent)
	})
	httpRouter := router.NewRouterForProfile(router.ProfileAll, router.Dependencies{
		Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
		API: router.APIDependencies{
			MeshXInternalAdmin: meshXAdmin,
		},
	})

	for _, test := range []struct {
		name       string
		token      string
		wantStatus int
	}{
		{name: "missing token", wantStatus: http.StatusUnauthorized},
		{name: "meshx service token", token: "meshx-admin-token", wantStatus: http.StatusNoContent},
	} {
		t.Run(test.name, func(t *testing.T) {
			response := httptest.NewRecorder()
			request := httptest.NewRequest(http.MethodGet, "/internal/admin/public-repository-caches", nil)
			if test.token != "" {
				request.Header.Set("Authorization", "Bearer "+test.token)
			}
			httpRouter.ServeHTTP(response, request)
			require.Equal(t, test.wantStatus, response.Code)
			require.Equal(t, "/internal/admin/public-repository-caches", <-seenPaths)
		})
	}
}

func TestWorkspaceProfileCanExposeDesignatedMeshXInternalAdmin(t *testing.T) {
	t.Parallel()
	httpRouter := router.NewRouterForProfile(router.ProfileWorkspace, router.Dependencies{
		Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
		API: router.APIDependencies{
			MeshXInternalAdmin: http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
				response.WriteHeader(http.StatusNoContent)
			}),
		},
	})
	response := httptest.NewRecorder()
	httpRouter.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/internal/admin/public-repository-caches", nil))
	require.Equal(t, http.StatusNoContent, response.Code)
}

func TestWorkspaceOwningProfilesMountWorkspaceQualifiedAGSSeamHandler(t *testing.T) {
	t.Parallel()
	for _, profile := range []router.Profile{router.ProfileAll, router.ProfileWorkspace} {
		profile := profile
		t.Run(string(profile), func(t *testing.T) {
			t.Parallel()
			seen := make(chan *http.Request, 1)
			next := http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
				seen <- request
				response.Header().Set("X-Test-Seam", "hit")
				response.WriteHeader(http.StatusTeapot)
			})
			httpRouter := router.NewRouterForProfile(profile, router.Dependencies{
				Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
				API:    router.APIDependencies{MeshXWorkspaceAGS: next},
			})
			request := httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/ags/api/v3/repos/acme/docs?ref=main", strings.NewReader("body"))
			request.Header.Set(api.HeaderRouteWorkspaceID, "wsp_1")
			request.Header.Set("X-AgentTeam-Resolved-Workspace-Group-Id", "group_1")
			response := httptest.NewRecorder()

			httpRouter.ServeHTTP(response, request)

			require.Equal(t, http.StatusTeapot, response.Code)
			require.Equal(t, "hit", response.Header().Get("X-Test-Seam"))
			forwarded := <-seen
			require.Equal(t, request.Method, forwarded.Method)
			require.Equal(t, "/api/v1/workspaces/wsp_1/ags/api/v3/repos/acme/docs?ref=main", forwarded.URL.RequestURI())
			require.Equal(t, request.Body, forwarded.Body)
		})
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	httpRouter := router.NewRouterForProfile(router.ProfileWorkspace, router.Dependencies{
		Logger: logger,
		API: router.APIDependencies{
			LLM: llm.Dependencies{APRDispatcher: runtime.NewDispatcher()},
		},
	})
	counts := routeCounts(httpRouter)

	for _, route := range []string{
		"GET /healthz",
		"GET /readyz",
		"GET /api/v1/workspaces/:workspaceId/providers",
		"GET /api/v1/workspaces/:workspaceId/selectable-models",
		"GET /api/v1/workspaces/:workspaceId/sidebar/channel-sections",
		"PUT /api/v1/workspaces/:workspaceId/sidebar/channel-sections",
		"GET /api/v1/workspaces/:workspaceId/channels",
		"GET /api/v1/workspaces/:workspaceId/apps",
		"GET /api/v1/workspaces/:workspaceId/app-installations/:installationId/service-event-subscriptions",
		"GET /api/v1/workspaces/:workspaceId/providers/github/config",
		"GET /api/v1/workspaces/:workspaceId/agents",
		"GET /api/v1/workspaces/:workspaceId/machines",
		"POST /api/apps/github/webhook",
		"POST /api/apps/gitea/webhook",
		"GET /api/v1/workspaces/:workspaceId/llm/healthz",
		"GET /api/v1/workspaces/:workspaceId/kb",
		"GET /api/v1/workspaces/:workspaceId/kb/*forwardPath",
		"GET /api/v1/workspaces/:workspaceId/skills/*forwardPath",
		"GET /api/v1/workspaces/:workspaceId/mcp/*forwardPath",
		"GET /api/v1/workspaces/:workspaceId/repositories/*forwardPath",
		"GET /api/v1/workspaces/:workspaceId/automations/*forwardPath",
	} {
		require.Equal(t, 1, counts[route], route)
	}
	for _, route := range []string{
		"GET /api/avatars/:avatarId",
		"GET /api/v1/public/config",
		"POST /api/v1/auth/login",
		"GET /api/v1/me",
		"GET /api/v1/admin/overview",
		"GET /api/v1/orgs",
		"GET /api/v1/workspaces",
		"POST /api/v1/machine-connect-exchanges",
		"POST /api/v1/machine-auth-resolutions",
		"GET /api/v1/me/model-profiles",
		"GET /api/v3/*forwardPath",
		"GET /api/ext/v1/*forwardPath",
		"GET /api/graphql",
		"GET /:owner/:repo/raw/*rawPath",
	} {
		require.Zero(t, counts[route], route)
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	dependencies := router.Dependencies{Logger: logger}
	allCounts := routeCounts(router.NewRouterForProfile(router.ProfileAll, dependencies))
	centralCounts := routeCounts(router.NewRouterForProfile(router.ProfileCentral, dependencies))
	workspaceCounts := routeCounts(router.NewRouterForProfile(router.ProfileWorkspace, dependencies))

	for _, route := range []string{
		"POST /api/v1/workspaces/:workspaceId/knowledge-qa/shares/open",
		"GET /api/v1/workspaces/:workspaceId/knowledge-qa/shares",
		"GET /api/v1/workspaces/:workspaceId/knowledge-qa/shares/:shareId",
		"GET /api/v1/workspaces/:workspaceId/knowledge-qa/shares/:shareId/evidence/:evidenceId",
	} {
		require.Equal(t, 1, allCounts[route], route)
		require.Equal(t, 1, workspaceCounts[route], route)
		require.Zero(t, centralCounts[route], route)
	}
	for _, route := range []string{
		"POST /api/v1/knowledge-qa/shares/open",
		"GET /api/v1/knowledge-qa/shares",
		"GET /api/v1/knowledge-qa/shares/:shareId",
		"GET /api/v1/knowledge-qa/shares/:shareId/evidence/:evidenceId",
	} {
		require.Zero(t, allCounts[route], route)
		require.Zero(t, centralCounts[route], route)
		require.Zero(t, workspaceCounts[route], route)
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	dependencies := router.Dependencies{
		Logger: logger,
		API: router.APIDependencies{
			WorkflowPlan: plan.Dependencies{
				Service:   planmanage.New(nil),
				AgentEdit: planmanage.NewAgentEditService(nil, nil, nil, nil, nil),
			},
		},
	}
	workspaceCounts := routeCounts(router.NewRouterForProfile(router.ProfileWorkspace, dependencies))
	centralCounts := routeCounts(router.NewRouterForProfile(router.ProfileCentral, dependencies))

	for _, route := range []string{
		"GET /api/v1/workspaces/:workspaceId/workflow/plans",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans:validate",
		"GET /api/v1/workspaces/:workspaceId/workflow/plans/:planId",
		"PATCH /api/v1/workspaces/:workspaceId/workflow/plans/:planId",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/archive",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/restore",
		"GET /api/v1/workspaces/:workspaceId/workflow/plans/:planId/draft",
		"PUT /api/v1/workspaces/:workspaceId/workflow/plans/:planId/draft",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/draft:validate",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/versions",
		"GET /api/v1/workspaces/:workspaceId/workflow/plans/:planId/versions",
		"GET /api/v1/workspaces/:workspaceId/workflow/plans/:planId/versions/:versionId",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/versions/:versionId/republish",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/versions/:versionId/make-default",
		"GET /api/v1/workspaces/:workspaceId/workflow/plans/:planId/agent-edit/messages",
		"GET /api/v1/workspaces/:workspaceId/workflow/plans/:planId/agent-edit/agents",
		"POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/agent-edit/requests",
	} {
		require.Equal(t, 1, workspaceCounts[route], route)
		require.Zero(t, centralCounts[route], route)
	}
	require.Zero(t, workspaceCounts["DELETE /api/v1/workspaces/:workspaceId/workflow/plans/:planId"])
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	counts := routeCounts(router.NewRouterForProfile(router.ProfileWorkspace, router.Dependencies{Logger: logger}))

	for _, route := range []string{
		"GET /api/v1/workspaces/:workspaceId/workflows",
		"POST /api/v1/workspaces/:workspaceId/workflows",
		"POST /api/v1/workspaces/:workspaceId/workflows:validate",
		"GET /api/v1/workspaces/:workspaceId/workflows/approvals",
		"GET /api/v1/workspaces/:workspaceId/workflows/:workflowId",
		"PATCH /api/v1/workspaces/:workspaceId/workflows/:workflowId",
		"DELETE /api/v1/workspaces/:workspaceId/workflows/:workflowId",
		"POST /api/v1/workspaces/:workspaceId/workflows/:workflowId/versions",
		"GET /api/v1/workspaces/:workspaceId/workflows/:workflowId/versions",
		"GET /api/v1/workspaces/:workspaceId/workflows/:workflowId/versions/:version",
		"GET /api/v1/workspaces/:workspaceId/workflows/:workflowId/runs",
		"GET /api/v1/workspaces/:workspaceId/workflows/:workflowId/runs/:runId",
		"GET /api/v1/workspaces/:workspaceId/workflows/:workflowId/runs/:runId/events",
		"POST /api/v1/workspaces/:workspaceId/workflows/:workflowId/runs/:runAction",
		"POST /api/v1/workspaces/:workspaceId/workflows/:workflowId/triggers/:triggerKey/runs",
	} {
		require.Zero(t, counts[route], route)
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	dependencies := router.Dependencies{
		Logger: logger,
		API: router.APIDependencies{
			WorkflowBinding: binding.Dependencies{Service: bindingmanage.New(nil, nil)},
		},
	}
	workspaceCounts := routeCounts(router.NewRouterForProfile(router.ProfileWorkspace, dependencies))
	centralCounts := routeCounts(router.NewRouterForProfile(router.ProfileCentral, dependencies))

	for _, route := range []string{
		"GET /api/v1/workspaces/:workspaceId/workflow/bindings",
		"POST /api/v1/workspaces/:workspaceId/workflow/bindings",
		"POST /api/v1/workspaces/:workspaceId/workflow/bindings:validate",
		"GET /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId",
		"PATCH /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId",
		"POST /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId/disable",
		"POST /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId/archive",
		"POST /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId/restore",
		"POST /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId/revisions",
		"GET /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId/revisions",
		"GET /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId/revisions/:revision",
		"POST /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId/revisions/:revision/activate",
	} {
		require.Equal(t, 1, workspaceCounts[route], route)
		require.Zero(t, centralCounts[route], route)
	}
	require.Zero(t, workspaceCounts["DELETE /api/v1/workspaces/:workspaceId/workflow/bindings/:bindingId"])
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	dependencies := router.Dependencies{
		Logger: logger,
		API: router.APIDependencies{
			WorkflowInvocation: invocation.Dependencies{Service: invocationmanage.New(nil)},
		},
	}
	workspaceCounts := routeCounts(router.NewRouterForProfile(router.ProfileWorkspace, dependencies))
	centralCounts := routeCounts(router.NewRouterForProfile(router.ProfileCentral, dependencies))

	for _, route := range []string{
		"POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/invocations",
		"GET /api/v1/workspaces/:workspaceId/workflow/invocations",
		"GET /api/v1/workspaces/:workspaceId/workflow/invocations/:invocationId",
	} {
		require.Equal(t, 1, workspaceCounts[route], route)
		require.Zero(t, centralCounts[route], route)
	}
	for _, method := range []string{http.MethodPatch, http.MethodDelete} {
		require.Zero(t, workspaceCounts[method+" /api/v1/workspaces/:workspaceId/workflow/invocations/:invocationId"])
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	dependencies := router.Dependencies{
		Logger: logger,
		API: router.APIDependencies{
			WorkflowRun: run.Dependencies{
				Service: runmanage.New(nil), Calendar: calendar.New(nil),
			},
		},
	}
	workspaceCounts := routeCounts(router.NewRouterForProfile(router.ProfileWorkspace, dependencies))
	centralCounts := routeCounts(router.NewRouterForProfile(router.ProfileCentral, dependencies))

	for _, route := range []string{
		"GET /api/v1/workspaces/:workspaceId/workflow/calendar",
		"GET /api/v1/workspaces/:workspaceId/workflow/runs",
		"GET /api/v1/workspaces/:workspaceId/workflow/runs/:runId",
		"GET /api/v1/workspaces/:workspaceId/workflow/runs/:runId/activations",
		"GET /api/v1/workspaces/:workspaceId/workflow/runs/:runId/effects",
		"GET /api/v1/workspaces/:workspaceId/workflow/runs/:runId/events",
		"POST /api/v1/workspaces/:workspaceId/workflow/runs/:runId/cancel",
	} {
		require.Equal(t, 1, workspaceCounts[route], route)
		require.Zero(t, centralCounts[route], route)
	}
	for _, method := range []string{http.MethodPost, http.MethodPatch, http.MethodPut, http.MethodDelete} {
		require.Zero(t, workspaceCounts[method+" /api/v1/workspaces/:workspaceId/workflow/runs"], method)
	}
	for _, route := range []string{
		"POST /api/v1/workspaces/:workspaceId/workflow/runs/:runId/retry",
		"DELETE /api/v1/workspaces/:workspaceId/workflow/runs/:runId",
	} {
		require.Zero(t, workspaceCounts[route], route)
	}
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	dependencies := router.Dependencies{
		Logger: logger,
		Agent: agent.Dependencies{
			Auth:                 routeAgentAuthenticator{},
			WorkflowRunCallbacks: routeWorkflowRunCallbackRuntime{},
		},
	}
	workspaceCounts := routeCounts(router.NewRouterForProfile(router.ProfileWorkspace, dependencies))
	centralCounts := routeCounts(router.NewRouterForProfile(router.ProfileCentral, dependencies))
	route := "POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/workflow/step"
	require.Equal(t, 1, workspaceCounts[route])
	require.Zero(t, centralCounts[route])
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	centralRouter := router.NewRouterForProfile(router.ProfileCentral, router.Dependencies{Logger: logger})
	workspaceRouter := router.NewRouterForProfile(router.ProfileWorkspace, router.Dependencies{
		Logger: logger,
		API: router.APIDependencies{
			LLM: llm.Dependencies{APRDispatcher: runtime.NewDispatcher()},
		},
	})

	pathConflicts, methodConflicts := profileRouteConflicts(centralRouter, workspaceRouter)

	expected := map[string]profileRouteConflict{
		"/healthz": {CentralMethods: []string{http.MethodGet}, WorkspaceMethods: []string{http.MethodGet}},
		"/readyz":  {CentralMethods: []string{http.MethodGet}, WorkspaceMethods: []string{http.MethodGet}},
	}
	require.Equal(t, expected, pathConflicts)

	expectedMethodConflicts := map[string][]string{
		"/healthz": {http.MethodGet},
		"/readyz":  {http.MethodGet},
	}
	require.Equal(t, expectedMethodConflicts, methodConflicts)
}

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

	centralRouter, workspaceRouter := edgeContractRouters(t)
	contract, err := router.BuildEdgeRouteContractForRouters(centralRouter, workspaceRouter)
	require.NoError(t, err)
	require.NotEmpty(t, contract)

	policies := make(map[string]router.EdgePolicy)
	for _, route := range contract {
		key := route.Profile + " " + route.Method + " " + route.Path
		require.NotEmpty(t, route.Policy, key)
		policies[key] = route.Policy
	}

	require.Equal(t, router.EdgePolicyHealth, policies["central GET /healthz"])
	require.Equal(t, router.EdgePolicyHealth, policies["workspace GET /readyz"])
	require.Equal(t, router.EdgePolicyCentralREST, policies["central GET /api/v1/public/config"])
	require.Equal(t, router.EdgePolicyCentralREST, policies["central POST /api/v1/auth/login"])
	require.Equal(t, router.EdgePolicyCentralException, policies["central GET /api/github/callback"])
	require.Equal(t, router.EdgePolicyCentralException, policies["central GET /api/github/setup/start"])
	require.Equal(t, router.EdgePolicyCentralREST, policies["central GET /api/avatars/:avatarId"])
	require.Equal(t, router.EdgePolicyCentralREST, policies["central GET /api/v1/users/:userId/avatar"])
	require.Equal(t, router.EdgePolicyCentralREST, policies["central GET /api/v1/me/avatar"])
	require.Equal(t, router.EdgePolicyCentralREST, policies["central PUT /api/v1/me/avatar"])
	require.Equal(t, router.EdgePolicyWorkspaceREST, policies["workspace GET /api/v1/workspaces/:workspaceId/agents/:agentId/avatar"])
	require.Equal(t, router.EdgePolicyCentralREST, policies["central GET /api/v1/workspaces"])
	require.Equal(t, router.EdgePolicyInternalOnly, policies["central GET /internal/admin/workspace-groups"])
	require.Equal(t, router.EdgePolicyInternalOnly, policies["workspace GET /internal/workspaces/:workspaceId/app-notifications/:notificationId"])
	require.Equal(t, router.EdgePolicyWorkspaceREST, policies["workspace GET /api/v1/workspaces/:workspaceId/channels"])
	require.Equal(t, router.EdgePolicyWorkspaceExplicit, policies["workspace POST /api/apps/github/webhook"])
	require.Equal(t, router.EdgePolicyWorkspaceExplicit, policies["workspace POST /api/apps/gitea/webhook"])
	require.Equal(t, router.EdgePolicyWorkspaceREST, policies["workspace GET /api/v1/workspaces/:workspaceId/llm/healthz"])
	// MeshX forwarder routes on workspace profile.
	require.Equal(t, router.EdgePolicyWorkspaceREST, policies["workspace GET /api/v1/workspaces/:workspaceId/kb"])
	require.Equal(t, router.EdgePolicyWorkspaceREST, policies["workspace GET /api/v1/workspaces/:workspaceId/memory"])
	require.Equal(t, router.EdgePolicyWorkspaceREST, policies["workspace GET /api/v1/workspaces/:workspaceId/skills"])
	require.Equal(t, router.EdgePolicyWorkspaceREST, policies["workspace GET /api/v1/workspaces/:workspaceId/mcp"])
	require.Equal(t, router.EdgePolicyWorkspaceREST, policies["workspace GET /api/v1/workspaces/:workspaceId/repositories"])
	require.NotContains(t, policies, "workspace GET /api/v3/*forwardPath")
	require.NotContains(t, policies, "workspace GET /api/graphql")
	require.NotContains(t, policies, "workspace GET /:owner/:repo/raw/*rawPath")
}

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

	centralRouter, workspaceRouter := edgeContractRouters(t)
	contract, err := router.BuildEdgeRouteContractForRouters(centralRouter, workspaceRouter)
	require.NoError(t, err)

	explicitPrefixes := map[router.EdgePolicy]map[string]bool{
		router.EdgePolicyCentralException:  make(map[string]bool),
		router.EdgePolicyWorkspaceExplicit: make(map[string]bool),
	}
	for _, route := range contract {
		switch route.Policy {
		case router.EdgePolicyCentralException:
			explicitPrefixes[route.Policy][edgePolicyPrefix(route.Path)] = true
		case router.EdgePolicyWorkspaceExplicit:
			explicitPrefixes[route.Policy][edgePolicyPrefix(route.Path)] = true
		}
	}

	require.Equal(t, map[string]bool{
		"/api/github/callback": true,
		"/api/github/setup":    true,
	}, explicitPrefixes[router.EdgePolicyCentralException])
	require.Equal(t, map[string]bool{
		"/api/apps/gitea/webhook":  true,
		"/api/apps/github/webhook": true,
	}, explicitPrefixes[router.EdgePolicyWorkspaceExplicit])
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	db := &fakeAdminDB{}
	admin := &fakeAdmin{}
	httpRouter := router.NewRouterForProfile(router.ProfileWorkspace, router.Dependencies{
		Logger: logger,
		Health: router.HealthDependencies{
			CoreDB:      db,
			CoreDBAdmin: admin,
		},
	})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/readyz", nil)
	httpRouter.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)

	var payload map[string]string
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &payload))
	require.Equal(t, "ready", payload["status"])
	require.Equal(t, "workspace", payload["profile"])
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	db := &fakeAdminDB{}
	admin := &fakeAdmin{}
	router := router.NewRouter(router.Dependencies{
		Logger: logger,
		Health: router.HealthDependencies{
			CoreDB:      db,
			CoreDBAdmin: admin,
		},
	})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/readyz", nil)
	router.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	require.True(t, db.pingCalled)
	require.True(t, admin.requireCalled)

	var payload map[string]string
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &payload))
	require.Equal(t, "ready", payload["status"])
}

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

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	router := router.NewRouter(router.Dependencies{
		Logger: logger,
		Health: router.HealthDependencies{
			CoreDB:      &fakeAdminDB{},
			CoreDBAdmin: &fakeAdmin{requireErr: errors.New("schema mismatch")},
		},
	})

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/readyz", nil)
	router.ServeHTTP(response, request)

	require.Equal(t, http.StatusServiceUnavailable, response.Code)

	var payload map[string]string
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &payload))
	require.Equal(t, "degraded", payload["status"])
	require.Equal(t, "schema mismatch", payload["error"])
}

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

	err := (router.Dependencies{}).ValidateForProfile(router.ProfileWorkspace)

	require.Error(t, err)
	require.Contains(t, err.Error(), "Health.CoreDB")
	require.Contains(t, err.Error(), "API.Channel.AuthStore")
	require.Contains(t, err.Error(), "API.IM.ChannelAccess")
	require.Contains(t, err.Error(), "API.IM.VisibleScopes")
	require.Contains(t, err.Error(), "API.WorkflowPlan.AuthStore")
	require.Contains(t, err.Error(), "API.WorkflowPlan.WorkspaceStore")
	require.Contains(t, err.Error(), "API.WorkflowPlan.Service")
	require.Contains(t, err.Error(), "API.WorkflowRun.AuthStore")
	require.Contains(t, err.Error(), "API.WorkflowRun.WorkspaceStore")
	require.Contains(t, err.Error(), "API.WorkflowRun.Service")
	require.Contains(t, err.Error(), "API.WorkflowRun.Calendar")
	require.Contains(t, err.Error(), "API.WorkflowBinding.AuthStore")
	require.Contains(t, err.Error(), "API.WorkflowBinding.WorkspaceStore")
	require.Contains(t, err.Error(), "API.WorkflowBinding.Service")
	require.Contains(t, err.Error(), "API.WorkflowInvocation.AuthStore")
	require.Contains(t, err.Error(), "API.WorkflowInvocation.WorkspaceStore")
	require.Contains(t, err.Error(), "API.WorkflowInvocation.Service")
	require.Contains(t, err.Error(), "API.LLM.APRDispatcher")
	require.Contains(t, err.Error(), "Agent.WorkflowPlans")
	require.Contains(t, err.Error(), "Agent.WorkflowPlanAgentEditGuard")
	require.Contains(t, err.Error(), "Agent.WorkflowBindings")
	require.Contains(t, err.Error(), "Agent.WorkflowBindingOwners")
	require.Contains(t, err.Error(), "Agent.WorkflowBindingWorkspace")
	require.Contains(t, err.Error(), "Agent.WorkflowInvocations")
	require.Contains(t, err.Error(), "Agent.WorkflowInvocationOwners")
	require.Contains(t, err.Error(), "Agent.WorkflowInvocationWorkspace")
	require.Contains(t, err.Error(), "Agent.WorkflowRuns")
	require.Contains(t, err.Error(), "Agent.WorkflowRunCallbacks")
	require.Contains(t, err.Error(), "Daemon.Hub")
	require.Contains(t, err.Error(), "Daemon.SessionIssuer")
	require.NotContains(t, err.Error(), "API.Auth.Store")
}

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

	err := (router.Dependencies{}).ValidateForProfile(router.ProfileCentral)

	require.Error(t, err)
	require.Contains(t, err.Error(), "API.Auth.Store")
	require.NotContains(t, err.Error(), "API.LLM.APRDispatcher")
	require.NotContains(t, err.Error(), "Agent.WorkflowPlans")
	require.NotContains(t, err.Error(), "Agent.WorkflowPlanAgentEditGuard")
	require.NotContains(t, err.Error(), "Agent.WorkflowBindings")
	require.NotContains(t, err.Error(), "Agent.WorkflowInvocations")
	require.NotContains(t, err.Error(), "Agent.WorkflowRuns")
	require.NotContains(t, err.Error(), "Daemon.Hub")
	require.NotContains(t, err.Error(), "Daemon.SessionIssuer")
	require.NotContains(t, err.Error(), "API.Bootstrap.AuthStore")
	require.NotContains(t, err.Error(), "API.Channel.AuthStore")
}

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

	store := &fakeRouterAdminStore{}
	err := (router.Dependencies{
		API: router.APIDependencies{
			Admin: adminapi.Dependencies{
				AuthStore:             store,
				AdminStore:            store,
				RegistrationAllowlist: store,
			},
		},
	}).Validate()

	require.Error(t, err)
	require.NotContains(t, err.Error(), "API.Admin.Store")
}

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

	err := (router.Dependencies{}).Validate()

	require.Error(t, err)
	require.Contains(t, err.Error(), "API.Task.Store")
}

func migratedDomainRoutes() []string {
	return []string{
		"POST /api/v1/auth/login",
		"POST /api/v1/auth/feishu/login-url",
		"POST /api/v1/auth/feishu/login",
		"POST /api/v1/auth/register",
		"POST /api/v1/auth/logout",
		"POST /api/v1/auth/refresh",
		"GET /api/v1/me/mobile-sessions",
		"DELETE /api/v1/me/mobile-sessions/:sessionFamilyId",
		"GET /api/v1/me",
		"PATCH /api/v1/me",
		"GET /api/v1/auth/mobile/history",
		"POST /api/v1/auth/mobile/request-verification",
		"POST /api/v1/auth/mobile/verify",
		"POST /api/v1/auth/verify-email",
		"POST /api/v1/auth/resend-verification",
		"POST /api/v1/auth/forgot-password",
		"POST /api/v1/auth/reset-password",
		"GET /api/v1/auth/invite-info",
		"GET /api/v1/auth/pending-invites/:inviteId",
		"GET /api/v1/auth/pending-invites",
		"POST /api/v1/auth/pending-invites/:inviteId/accept",
		"POST /api/v1/auth/pending-invites/:inviteId/dismiss",
		"POST /api/v1/auth/accept-invite",
		"GET /api/v1/auth/oidc/discovery",
		"POST /api/v1/auth/oidc/authorize-url",
		"POST /api/v1/auth/oidc/callback",
		"GET /api/v1/admin/overview",
		"GET /api/v1/admin/workspaces",
		"GET /api/v1/admin/users",
		"GET /api/v1/admin/registration-allowlist",
		"POST /api/v1/admin/registration-allowlist",
		"POST /api/v1/admin/registration-allowlist/:entryId/disable",
		"GET /api/v1/admin/runtime/db-pool",
		"PATCH /api/v1/admin/runtime/db-pool",
		"GET /api/v1/me/behavior-settings",
		"PATCH /api/v1/me/behavior-settings",
		"GET /api/v1/billing/summary",
		"GET /api/v1/billing/ledger",
		"GET /api/v1/billing/payments",
		"GET /api/v1/billing/pricing",
		"POST /api/v1/billing/credit-recharge-orders",
		"GET /api/v1/billing/credit-recharge-orders/:orderId",
		"POST /api/v1/billing/credit-recharge-orders/:orderId/sync",
		"POST /api/v1/billing/alipay/notify",
		"GET /api/v1/billing/alipay/return",
		"GET /api/v1/workspaces",
		"POST /api/v1/workspaces",
		"GET /api/v1/workspaces/:workspaceId/organization",
		"GET /api/v1/workspaces/:workspaceId/members",
		"POST /api/v1/workspaces/:workspaceId/members/search",
		"POST /api/v1/workspaces/:workspaceId/members:batchLookup",
		"GET /api/v1/workspaces/:workspaceId/members/by-user/:userId/offboarding-impact",
		"GET /api/v1/workspaces/:workspaceId/members/:memberId",
		"PATCH /api/v1/workspaces/:workspaceId/members/:memberId",
		"DELETE /api/v1/workspaces/:workspaceId/members/:memberId",
		"GET /api/v1/workspaces/:workspaceId/invites",
		"POST /api/v1/workspaces/:workspaceId/invites",
		"DELETE /api/v1/workspaces/:workspaceId/invites/:inviteId",
		"GET /api/v1/workspaces/:workspaceId/usage",
		"POST /api/v1/machine-connect-exchanges",
		"POST /api/v1/machine-auth-resolutions",
		"GET /api/v1/workspaces/:workspaceId/machines",
		"POST /api/v1/workspaces/:workspaceId/machines",
		"PATCH /api/v1/workspaces/:workspaceId/machines/:machineId",
		"DELETE /api/v1/workspaces/:workspaceId/machines/:machineId",
		"POST /api/v1/workspaces/:workspaceId/machines/:machineId/api-key-rotations",
		"POST /api/v1/workspaces/:workspaceId/machines/:machineId/connect-tokens",
		"POST /api/v1/workspaces/:workspaceId/machines/:machineId/evictions",
		"GET /api/v1/workspaces/:workspaceId/machines/:machineId/agents",
		"GET /api/v1/workspaces/:workspaceId/channels/:channelId/threads",
		"GET /api/v1/workspaces/:workspaceId/threads/count",
		"GET /api/v1/workspaces/:workspaceId/threads",
		"GET /api/v1/workspaces/:workspaceId/threads/:threadChannelId/membership",
		"GET /api/v1/workspaces/:workspaceId/threads/:threadChannelId",
		"GET /api/v1/workspaces/:workspaceId/machines/:machineId/workspace-directories",
		"DELETE /api/v1/workspaces/:workspaceId/machines/:machineId/workspace-directories/:directoryName",
		"POST /api/v1/workspaces/:workspaceId/machines/:machineId/directories:list",
		"GET /api/avatars/:avatarId",
		"GET /api/v1/public/config",
		"POST /api/v1/public/content-reports",
		"GET /api/v1/me/avatar",
		"PUT /api/v1/me/avatar",
		"GET /api/v1/users/:userId/avatar",
		"POST /api/v1/workspaces/:workspaceId/attachments",
		"GET /api/v1/workspaces/:workspaceId/attachments/:attachmentId",
		"GET /api/v1/workspaces/:workspaceId/agents/:agentId/avatar",
		"GET /api/v1/workspaces/:workspaceId/channels/:channelId/threads",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/threads",
		"GET /api/v1/workspaces/:workspaceId/threads",
		"GET /api/v1/workspaces/:workspaceId/threads/:threadChannelId",
		"POST /api/v1/workspaces/:workspaceId/threads/:threadChannelId/subscribe",
		"POST /api/v1/workspaces/:workspaceId/threads/:threadChannelId/unsubscribe",
		"GET /api/v1/workspaces/:workspaceId/stickers",
		"GET /api/v1/workspaces/:workspaceId/stickers/:stickerId",
		"POST /api/v1/workspaces/:workspaceId/stickers",
		"DELETE /api/v1/workspaces/:workspaceId/stickers/:stickerId",
		"GET /api/v1/workspaces/:workspaceId/channels",
		"POST /api/v1/workspaces/:workspaceId/channels",
		"GET /api/v1/workspaces/:workspaceId/channels/discover",
		"GET /api/v1/workspaces/:workspaceId/channels/discover/:channelId",
		"PATCH /api/v1/workspaces/:workspaceId/channels/:channelId",
		"DELETE /api/v1/workspaces/:workspaceId/channels/:channelId",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/owner",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/join",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/archive",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/unarchive",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/leave",
		"GET /api/v1/workspaces/:workspaceId/channels/dm",
		"POST /api/v1/workspaces/:workspaceId/channels/dm",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/owner-context-dismiss",
		"GET /api/v1/workspaces/:workspaceId/channels/:channelId/addable-agents",
		"GET /api/v1/workspaces/:workspaceId/channels/:channelId/addable-people",
		"GET /api/v1/workspaces/:workspaceId/channels/:channelId/members",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/members",
		"POST /api/v1/workspaces/:workspaceId/channels/:channelId/members/batch",
		"DELETE /api/v1/workspaces/:workspaceId/channels/:channelId/members/agent/:agentId",
		"DELETE /api/v1/workspaces/:workspaceId/channels/:channelId/members/service/:memberId",
		"DELETE /api/v1/workspaces/:workspaceId/channels/:channelId/members/user/:userId",
		"GET /api/v1/workspaces/:workspaceId/kb",
		"GET /api/v1/workspaces/:workspaceId/kb/*forwardPath",
		"GET /api/v1/workspaces/:workspaceId/task-board/options",
		"POST /api/v1/workspaces/:workspaceId/task-board/options",
		"PATCH /api/v1/workspaces/:workspaceId/task-board/options/:optionId",
		"POST /api/v1/workspaces/:workspaceId/task-board/options/:optionId/reorder",
		"POST /api/v1/workspaces/:workspaceId/task-board/options/:optionId/deactivate",
		"POST /api/v1/workspaces/:workspaceId/task-board/options/:optionId/reactivate",
		"GET /api/v1/workspaces/:workspaceId/task-board/saved-views",
		"POST /api/v1/workspaces/:workspaceId/task-board/saved-views",
		"PATCH /api/v1/workspaces/:workspaceId/task-board/saved-views/:savedViewId",
		"DELETE /api/v1/workspaces/:workspaceId/task-board/saved-views/:savedViewId",
		"POST /api/v1/workspaces/:workspaceId/tasks/workspace/browse",
		"POST /api/v1/workspaces/:workspaceId/tasks/workspace/board/browse",
		"GET /api/v1/workspaces/:workspaceId/tasks/channel/:channelId/view-state",
		"PUT /api/v1/workspaces/:workspaceId/tasks/channel/:channelId/view-state",
		"POST /api/v1/workspaces/:workspaceId/tasks/channel/:channelId/browse",
		"POST /api/v1/workspaces/:workspaceId/tasks/channel/:channelId/board/browse",
		"GET /api/v1/workspaces/:workspaceId/tasks/:taskId/timeline",
		"PATCH /api/v1/workspaces/:workspaceId/tasks/:taskId",
		"PATCH /api/v1/workspaces/:workspaceId/tasks/:taskId/claim",
		"PATCH /api/v1/workspaces/:workspaceId/tasks/:taskId/unclaim",
		"PATCH /api/v1/workspaces/:workspaceId/tasks/:taskId/block",
		"PATCH /api/v1/workspaces/:workspaceId/tasks/:taskId/unblock",
		"POST /api/v1/workspaces/:workspaceId/tasks/:taskId/subtasks",
		"POST /api/v1/workspaces/:workspaceId/tasks/:taskId/checkpoints",
		"DELETE /api/v1/workspaces/:workspaceId/tasks/:taskId",
		"GET /api/v1/workspaces/:workspaceId/schedules",
		"POST /api/v1/workspaces/:workspaceId/schedules",
		"POST /api/v1/workspaces/:workspaceId/schedules:validate",
		"GET /api/v1/workspaces/:workspaceId/schedules/:scheduleId",
		"PATCH /api/v1/workspaces/:workspaceId/schedules/:scheduleId",
		"POST /api/v1/workspaces/:workspaceId/schedules/:scheduleAction",
		"DELETE /api/v1/workspaces/:workspaceId/schedules/:scheduleId",
		"GET /api/v1/workspaces/:workspaceId/schedules/:scheduleId/runs",
		"GET /api/v1/workspaces/:workspaceId/triggers",
		"POST /api/v1/workspaces/:workspaceId/triggers",
		"POST /api/v1/workspaces/:workspaceId/triggers:validate",
		"GET /api/v1/workspaces/:workspaceId/triggers/:triggerId",
		"PATCH /api/v1/workspaces/:workspaceId/triggers/:triggerId",
		"POST /api/v1/workspaces/:workspaceId/triggers/:triggerAction",
		"DELETE /api/v1/workspaces/:workspaceId/triggers/:triggerId",
		"GET /api/v1/workspaces/:workspaceId/triggers/:triggerId/runs",
		"GET /api/v1/workspaces/:workspaceId/apps",
		"GET /api/v1/workspaces/:workspaceId/app-installations",
		"DELETE /api/v1/workspaces/:workspaceId/app-installations/:installationId",
		"PATCH /api/v1/workspaces/:workspaceId/app-installations/:installationId/display-name",
		"GET /api/v1/workspaces/:workspaceId/app-installations/:installationId/runner-credentials",
		"POST /api/v1/workspaces/:workspaceId/app-installations/:installationId/runner-credentials",
		"GET /api/v1/workspaces/:workspaceId/app-installations/:installationId/initial-credentials",
		"POST /api/v1/workspaces/:workspaceId/app-installations/:installationId/initial-credentials/claim",
		"GET /api/v1/workspaces/:workspaceId/app-installation-requests",
		"POST /api/v1/workspaces/:workspaceId/app-installation-requests",
		"POST /api/v1/workspaces/:workspaceId/app-installation-requests/:requestAction",
		"GET /api/v1/workspaces/:workspaceId/app-installations/:installationId/service-event-subscriptions",
		"POST /api/v1/workspaces/:workspaceId/app-installations/:installationId/service-event-subscriptions",
		"GET /api/v1/workspaces/:workspaceId/app-installations/:installationId/provider-watches",
		"POST /api/v1/workspaces/:workspaceId/app-installations/:installationId/provider-watches",
		"GET /api/v1/workspaces/:workspaceId/app-webhook-deliveries",
		"POST /api/v1/workspaces/:workspaceId/service-event-subscriptions/:subscriptionAction",
		"DELETE /api/v1/workspaces/:workspaceId/service-event-subscriptions/:subscriptionId",
		"POST /api/v1/workspaces/:workspaceId/provider-watches/:watchAction",
		"DELETE /api/v1/workspaces/:workspaceId/provider-watches/:watchId",
		"GET /api/v1/workspaces/:workspaceId/agents/:agentId/app-authorizations",
		"POST /api/v1/workspaces/:workspaceId/agents/:agentId/app-authorizations",
		"DELETE /api/v1/workspaces/:workspaceId/agents/:agentId/app-authorizations/:appInstallationId",
	}
}

func organizationDomainRoutes() []string {
	return []string{
		"GET /api/v1/orgs",
		"PATCH /api/v1/orgs/:orgId",
		"GET /api/v1/orgs/:orgId/settings",
		"GET /api/v1/orgs/:orgId/plan",
		"GET /api/v1/orgs/:orgId/directory-sync-runs/:syncRunId",
		"PATCH /api/v1/orgs/:orgId/settings",
		"GET /api/v1/orgs/:orgId/enterprise/feishu",
		"PUT /api/v1/orgs/:orgId/enterprise/feishu/credentials",
		"DELETE /api/v1/orgs/:orgId/enterprise/feishu/credentials",
		"POST /api/v1/orgs/:orgId/feishu/sync",
		"GET /api/v1/orgs/:orgId/invites",
		"POST /api/v1/orgs/:orgId/invites",
		"DELETE /api/v1/orgs/:orgId/invites/:inviteId",
		"GET /api/v1/orgs/:orgId/units",
		"POST /api/v1/orgs/:orgId/units",
		"GET /api/v1/orgs/:orgId/units/:unitId/members",
		"POST /api/v1/orgs/:orgId/units/:unitId/members",
		"DELETE /api/v1/orgs/:orgId/units/:unitId/members/:memberId",
		"GET /api/v1/orgs/:orgId/teams",
		"POST /api/v1/orgs/:orgId/teams",
		"GET /api/v1/orgs/:orgId/teams/:teamId/members",
		"POST /api/v1/orgs/:orgId/teams/:teamId/members",
		"DELETE /api/v1/orgs/:orgId/teams/:teamId/members/:memberId",
		"GET /api/v1/orgs/:orgId/members",
		"POST /api/v1/orgs/:orgId/members",
		"GET /api/v1/orgs/:orgId/members/:memberId/removal-impact",
		"DELETE /api/v1/orgs/:orgId/members/:memberId",
		"GET /api/v1/orgs/:orgId/members/:memberId/units",
		"PATCH /api/v1/orgs/:orgId/members/:memberId/primary-team",
		"PATCH /api/v1/orgs/:orgId/members/:memberId/role",
		"GET /api/v1/orgs/:orgId/workspaces",
		"POST /api/v1/orgs/:orgId/workspaces",
		"GET /api/v1/orgs/:orgId/providers",
		"GET /api/v1/orgs/:orgId/llm-stats/summary",
		"GET /api/v1/orgs/:orgId/llm-stats/detail",
		"GET /api/v1/orgs/:orgId/llm-stats/timeseries",
		"GET /api/v1/orgs/:orgId/llm-stats/errors/summary",
		"GET /api/v1/orgs/:orgId/llm-stats/errors/timeseries",
		"GET /api/v1/orgs/:orgId/llm-quota-policies",
		"PUT /api/v1/orgs/:orgId/llm-quota-policies/:subjectType/:subjectId",
		"DELETE /api/v1/orgs/:orgId/llm-quota-policies/:subjectType/:subjectId",
		"GET /api/v1/orgs/:orgId/llm-proxy-keys",
		"POST /api/v1/orgs/:orgId/llm-proxy-keys",
		"DELETE /api/v1/orgs/:orgId/llm-proxy-keys/:keyId",
		"GET /api/v1/orgs/:orgId/provider-routes",
		"POST /api/v1/orgs/:orgId/provider-routes",
		"PATCH /api/v1/orgs/:orgId/provider-routes/:routeId",
		"DELETE /api/v1/orgs/:orgId/provider-routes/:routeId",
		"GET /api/v1/orgs/:orgId/provider-routes/:routeId/groups",
		"POST /api/v1/orgs/:orgId/provider-routes/:routeId/groups",
		"PATCH /api/v1/orgs/:orgId/provider-routes/:routeId/groups/:groupId",
		"DELETE /api/v1/orgs/:orgId/provider-routes/:routeId/groups/:groupId",
		"POST /api/v1/orgs/:orgId/providers",
		"GET /api/v1/orgs/:orgId/providers/:providerId",
		"PATCH /api/v1/orgs/:orgId/providers/:providerId",
		"DELETE /api/v1/orgs/:orgId/providers/:providerId",
		"GET /api/v1/orgs/:orgId/providers/:providerId/tokens",
		"POST /api/v1/orgs/:orgId/providers/:providerId/tokens",
		"DELETE /api/v1/orgs/:orgId/providers/:providerId/tokens/:tokenId",
		"GET /api/v1/orgs/:orgId/providers/:providerId/codex/auth",
		"POST /api/v1/orgs/:orgId/providers/:providerId/codex/auth/refresh",
		"GET /api/v1/orgs/:orgId/workspaces/:workspaceId/members",
		"GET /api/v1/orgs/:orgId/workspaces/:workspaceId/providers",
		"POST /api/v1/orgs/:orgId/workspaces/:workspaceId/providers/:providerId",
		"DELETE /api/v1/orgs/:orgId/workspaces/:workspaceId/providers/:providerId",
		"GET /api/v1/orgs/:orgId/workspaces/:workspaceId/provider-routes",
		"POST /api/v1/orgs/:orgId/workspaces/:workspaceId/provider-routes/:routeId",
		"DELETE /api/v1/orgs/:orgId/workspaces/:workspaceId/provider-routes/:routeId",
		"GET /api/v1/orgs/:orgId/workspaces/:workspaceId/teams",
		"POST /api/v1/orgs/:orgId/workspaces/:workspaceId/teams/:teamId",
		"DELETE /api/v1/orgs/:orgId/workspaces/:workspaceId/teams/:teamId",
		"POST /api/v1/orgs/:orgId/workspaces/:workspaceId/members/:memberId",
		"DELETE /api/v1/orgs/:orgId/workspaces/:workspaceId/members/:memberId",
	}
}

type profileRouteConflict struct {
	CentralMethods   []string
	WorkspaceMethods []string
}

func profileRouteConflicts(centralRouter *gin.Engine, workspaceRouter *gin.Engine) (map[string]profileRouteConflict, map[string][]string) {
	centralRoutes := profileRoutesByPath(centralRouter)
	workspaceRoutes := profileRoutesByPath(workspaceRouter)
	pathConflicts := make(map[string]profileRouteConflict)
	methodConflicts := make(map[string][]string)
	for path, centralMethods := range centralRoutes {
		workspaceMethods, ok := workspaceRoutes[path]
		if !ok {
			continue
		}
		centralList := sortedMethodList(centralMethods)
		workspaceList := sortedMethodList(workspaceMethods)
		pathConflicts[path] = profileRouteConflict{
			CentralMethods:   centralList,
			WorkspaceMethods: workspaceList,
		}
		for method := range centralMethods {
			if workspaceMethods[method] {
				methodConflicts[path] = append(methodConflicts[path], method)
			}
		}
		sort.Strings(methodConflicts[path])
		if len(methodConflicts[path]) == 0 {
			delete(methodConflicts, path)
		}
	}
	return pathConflicts, methodConflicts
}

func profileRoutesByPath(router *gin.Engine) map[string]map[string]bool {
	routes := make(map[string]map[string]bool)
	for _, route := range router.Routes() {
		methods, ok := routes[route.Path]
		if !ok {
			methods = make(map[string]bool)
			routes[route.Path] = methods
		}
		methods[route.Method] = true
	}
	return routes
}

func sortedMethodList(methods map[string]bool) []string {
	list := make([]string, 0, len(methods))
	for method := range methods {
		list = append(list, method)
	}
	sort.Strings(list)
	return list
}

func edgeContractRouters(t *testing.T) (*gin.Engine, *gin.Engine) {
	t.Helper()

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	liveEmitter := im.NewLiveEventEmitter(im.NewStreamEventHub(), nil, logger)

	centralRouter := router.NewRouterForProfile(router.ProfileCentral, router.Dependencies{Logger: logger})
	workspaceRouter := router.NewRouterForProfile(router.ProfileWorkspace, router.Dependencies{
		Logger:      logger,
		LiveEmitter: liveEmitter,
		API: router.APIDependencies{
			LLM: llm.Dependencies{APRDispatcher: runtime.NewDispatcher()},
		},
	})
	return centralRouter, workspaceRouter
}

func edgePolicyPrefix(path string) string {
	switch {
	case strings.HasPrefix(path, "/api/github/setup"):
		return "/api/github/setup"
	case strings.HasPrefix(path, "/api/llm/"):
		return "/api/llm"
	case strings.HasPrefix(path, "/socket.io/"):
		return "/socket.io"
	case strings.HasPrefix(path, "/w/") && strings.Contains(path, "/socket.io"):
		return "/socket.io"
	case strings.HasPrefix(path, "/:owner/:repo/raw/"):
		return "/:owner/:repo/raw"
	default:
		return path
	}
}

func routeCounts(router *gin.Engine) map[string]int {
	counts := make(map[string]int)
	for _, route := range router.Routes() {
		counts[route.Method+" "+route.Path]++
	}
	return counts
}

func observedRouterLogger() (*slog.Logger, *bytes.Buffer) {
	var logs bytes.Buffer
	handler := slog.NewJSONHandler(&logs, nil)
	return slog.New(logging.WrapHandler(handler)), &logs
}

func loggedEntry(t *testing.T, logs *bytes.Buffer) map[string]any {
	t.Helper()
	entries := loggedEntries(t, logs)
	require.NotEmpty(t, entries)
	return entries[len(entries)-1]
}

func loggedEntries(t *testing.T, logs *bytes.Buffer) []map[string]any {
	t.Helper()
	lines := bytes.Split(bytes.TrimSpace(logs.Bytes()), []byte("\n"))
	entries := make([]map[string]any, 0, len(lines))
	for _, line := range lines {
		if len(bytes.TrimSpace(line)) == 0 {
			continue
		}
		var entry map[string]any
		require.NoError(t, json.Unmarshal(line, &entry))
		entries = append(entries, entry)
	}
	return entries
}

func newRecordingHTTPTracing() (*tracing.HTTPServer, *recordingTracerProvider) {
	recorder := &recordingTracerProvider{}
	return tracing.NewHTTPServer(recorder.Tracer("test"), tracing.NewPropagator()), recorder
}

type recordingTracerProvider struct {
	embedded.TracerProvider
	spans      []*recordedSpan
	nextTrace  byte
	nextSpanID byte
}

func (provider *recordingTracerProvider) Tracer(string, ...trace.TracerOption) trace.Tracer {
	return &recordingTracer{provider: provider}
}

func (provider *recordingTracerProvider) endedSpans() []*recordedSpan {
	ended := make([]*recordedSpan, 0, len(provider.spans))
	for _, span := range provider.spans {
		if span.ended {
			ended = append(ended, span)
		}
	}
	return ended
}

func (provider *recordingTracerProvider) newTraceID() trace.TraceID {
	provider.nextTrace++
	var traceID trace.TraceID
	traceID[15] = provider.nextTrace
	return traceID
}

func (provider *recordingTracerProvider) newSpanID() trace.SpanID {
	provider.nextSpanID++
	var spanID trace.SpanID
	spanID[7] = provider.nextSpanID
	return spanID
}

type recordingTracer struct {
	embedded.Tracer
	provider *recordingTracerProvider
}

func (tracer *recordingTracer) Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
	cfg := trace.NewSpanStartConfig(opts...)
	parent := trace.SpanContextFromContext(ctx)
	traceID := parent.TraceID()
	traceFlags := parent.TraceFlags()
	if !parent.IsValid() || cfg.NewRoot() {
		traceID = tracer.provider.newTraceID()
		traceFlags = trace.FlagsSampled
	}
	span := &recordedSpan{
		provider: tracer.provider,
		name:     name,
		parent:   parent,
		kind:     cfg.SpanKind(),
		attrs:    append([]attribute.KeyValue(nil), cfg.Attributes()...),
		spanContext: trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    traceID,
			SpanID:     tracer.provider.newSpanID(),
			TraceFlags: traceFlags,
		}),
	}
	tracer.provider.spans = append(tracer.provider.spans, span)
	return trace.ContextWithSpan(ctx, span), span
}

type recordedSpan struct {
	embedded.Span
	provider    trace.TracerProvider
	name        string
	spanContext trace.SpanContext
	parent      trace.SpanContext
	kind        trace.SpanKind
	attrs       []attribute.KeyValue
	status      codes.Code
	statusText  string
	errors      []error
	ended       bool
}

func (span *recordedSpan) End(...trace.SpanEndOption) {
	span.ended = true
}

func (span *recordedSpan) AddEvent(string, ...trace.EventOption) {}

func (span *recordedSpan) AddLink(trace.Link) {}

func (span *recordedSpan) IsRecording() bool {
	return !span.ended
}

func (span *recordedSpan) RecordError(err error, _ ...trace.EventOption) {
	if err != nil {
		span.errors = append(span.errors, err)
	}
}

func (span *recordedSpan) SpanContext() trace.SpanContext {
	return span.spanContext
}

func (span *recordedSpan) SetStatus(code codes.Code, description string) {
	span.status = code
	span.statusText = description
}

func (span *recordedSpan) SetName(name string) {
	span.name = name
}

func (span *recordedSpan) SetAttributes(attrs ...attribute.KeyValue) {
	span.attrs = append(span.attrs, attrs...)
}

func (span *recordedSpan) TracerProvider() trace.TracerProvider {
	return span.provider
}

func spanStringAttribute(t *testing.T, span *recordedSpan, key string) string {
	t.Helper()
	for _, attr := range span.attrs {
		if string(attr.Key) == key {
			return attr.Value.AsString()
		}
	}
	require.Failf(t, "missing span attribute", "key %q", key)
	return ""
}

func spanIntAttribute(t *testing.T, span *recordedSpan, key string) int64 {
	t.Helper()
	for _, attr := range span.attrs {
		if string(attr.Key) == key {
			return attr.Value.AsInt64()
		}
	}
	require.Failf(t, "missing span attribute", "key %q", key)
	return 0
}

func metricCount(t *testing.T, reg *prometheus.Registry, name string) int {
	t.Helper()
	families, err := reg.Gather()
	require.NoError(t, err)
	for _, family := range families {
		if family.GetName() == name {
			return len(family.GetMetric())
		}
	}
	return 0
}

func metricValue(t *testing.T, reg *prometheus.Registry, name string, labels map[string]string) float64 {
	t.Helper()
	metric := findMetric(t, reg, name, labels)
	if metric == nil {
		return 0
	}
	switch {
	case metric.GetGauge() != nil:
		return metric.GetGauge().GetValue()
	case metric.GetCounter() != nil:
		return metric.GetCounter().GetValue()
	default:
		return 0
	}
}

func findMetric(t *testing.T, reg *prometheus.Registry, name string, labels map[string]string) *dto.Metric {
	t.Helper()
	families, err := reg.Gather()
	require.NoError(t, err)
	for _, family := range families {
		if family.GetName() != name {
			continue
		}
		for _, metric := range family.GetMetric() {
			if metricLabelsMatch(metric, labels) {
				return metric
			}
		}
	}
	return nil
}

func metricLabelsMatch(metric *dto.Metric, labels map[string]string) bool {
	if len(metric.GetLabel()) != len(labels) {
		return false
	}
	for _, label := range metric.GetLabel() {
		if labels[label.GetName()] != label.GetValue() {
			return false
		}
	}
	return true
}

type fakeRouterAdminStore struct{}

func (fake *fakeRouterAdminStore) AuthenticateSession(context.Context, string) (identity.AuthenticatedUser, error) {
	return identity.AuthenticatedUser{}, nil
}

func (fake *fakeRouterAdminStore) ListAdminUsers(context.Context, string, string, time.Time, bool, int) ([]admin.AdminUserSummary, error) {
	return nil, nil
}

func (fake *fakeRouterAdminStore) ListAdminWorkspaces(context.Context, string, string, time.Time, bool, int) ([]admin.AdminWorkspaceSummary, error) {
	return nil, nil
}

func (fake *fakeRouterAdminStore) LoadAdminOverview(context.Context, string, time.Time) (admin.AdminOverview, error) {
	return admin.AdminOverview{}, nil
}

func (fake *fakeRouterAdminStore) CreateRegistrationAllowlistEntryForUser(context.Context, string, string, string, *time.Time) (identity.RegistrationAllowlistEntry, error) {
	return identity.RegistrationAllowlistEntry{}, nil
}

func (fake *fakeRouterAdminStore) DisableRegistrationAllowlistEntryForUser(context.Context, string, string) (identity.RegistrationAllowlistEntry, error) {
	return identity.RegistrationAllowlistEntry{}, nil
}

func (fake *fakeRouterAdminStore) ListRegistrationAllowlistEntriesForUser(context.Context, string, string, bool) ([]identity.RegistrationAllowlistEntry, error) {
	return nil, nil
}

type fakeAdminDB struct {
	pingCalled bool
	pingErr    error
}

func (db *fakeAdminDB) Ping(ctx context.Context) error {
	db.pingCalled = true
	return db.pingErr
}

func (db *fakeAdminDB) Close() {}

type fakeAdmin struct {
	requireCalled bool
	requireErr    error
}

func (admin *fakeAdmin) Database() string {
	return "test"
}

func (admin *fakeAdmin) Open(context.Context, string, time.Duration) (coredb.AdminDB, error) {
	return &fakeAdminDB{}, nil
}

func (admin *fakeAdmin) ApplyMigrations(context.Context, coredb.AdminDB) error {
	return nil
}

func (admin *fakeAdmin) InspectMigrationState(context.Context, coredb.AdminDB) (coredb.MigrationState, error) {
	return coredb.MigrationState{}, nil
}

func (admin *fakeAdmin) VerifySchema(context.Context, coredb.AdminDB) error {
	return nil
}

func (admin *fakeAdmin) RequireSchemaCurrent(context.Context, coredb.AdminDB) error {
	admin.requireCalled = true
	return admin.requireErr
}

func (admin *fakeAdmin) RunStartupPreflight(context.Context, coredb.AdminDB) error {
	return nil
}

func (admin *fakeAdmin) ClassifyRetryable(error) bool {
	return false
}
