using System.Linq;
using System.Text.Json;
using Motus.Abstractions;
using Motus.Mcp;

namespace Motus.Mcp.Tests.Tools;

/// <summary>
/// An <see cref="ActivePageService"/> that serves a fixed fake page, so the tools
/// can be exercised without a browser. The snapshot registry, gating, and
/// invalidation run for real against the fake page.
/// </summary>
internal sealed class FakeActivePageService(FakeToolPage page)
    : ActivePageService(new BrowserSessionManager(new McpServerLaunchOptions()))
{
    public FakeToolPage Page { get; } = page;

    protected override Task<IPage> ResolvePageAsync(CancellationToken cancellationToken)
        => Task.FromResult<IPage>(Page);
}

/// <summary>
/// A fake page that serves a fixed accessibility snapshot, hands out a single
/// recording locator for any backend node id, and records navigation and
/// screenshot calls. Every other member is unused.
/// </summary>
internal sealed class FakeToolPage(AccessibilitySnapshot snapshot) : IPage
{
    public FakeToolLocator RecordingLocator { get; } = new();

    /// <summary>The last URL passed to <see cref="GotoAsync"/>.</summary>
    public string? NavigatedUrl { get; private set; }

    /// <summary>The last backend node id resolved through <see cref="LocatorByBackendNodeId"/>.</summary>
    public long? ResolvedBackendNodeId { get; private set; }

    /// <summary>The <c>FullPage</c> flag of the last screenshot request.</summary>
    public bool? ScreenshotFullPage { get; private set; }

    /// <summary>When set, <see cref="GotoAsync"/> throws this to simulate a failed navigation.</summary>
    public Exception? GotoError { get; init; }

    /// <summary>The page-level keyboard, recording the keys <c>press_key</c> sends.</summary>
    public FakeKeyboard FakeKeyboard { get; } = new();

    /// <summary>The page-level mouse, recording the events the coordinate tools dispatch.</summary>
    public FakeMouse FakeMouse { get; } = new();

    /// <summary>The last viewport passed to <see cref="SetViewportSizeAsync"/>.</summary>
    public ViewportSize? ResizedTo { get; private set; }

    /// <summary>The last timeout passed to <see cref="WaitForTimeoutAsync"/>.</summary>
    public double? WaitedTimeoutMs { get; private set; }

    /// <summary>The function expressions passed to <see cref="WaitForFunctionAsync{T}"/>.</summary>
    public List<string> WaitedFunctions { get; } = [];

    /// <summary>The arguments passed alongside each <see cref="WaitForFunctionAsync{T}"/> call.</summary>
    public List<object?> WaitedFunctionArgs { get; } = [];

    /// <summary>The current URL, returned by the <see cref="Url"/> property.</summary>
    public string PageUrl { get; set; } = "about:blank";

    /// <summary>The title returned by <see cref="TitleAsync"/>.</summary>
    public string PageTitle { get; set; } = "";

    /// <summary>The response <see cref="GoBackAsync"/> returns; null models no history entry.</summary>
    public IResponse? GoBackResponse { get; set; }

    /// <summary>The response <see cref="GoForwardAsync"/> returns; null models no history entry.</summary>
    public IResponse? GoForwardResponse { get; set; }

    /// <summary>Whether <see cref="GoBackAsync"/> was called.</summary>
    public bool GoBackCalled { get; private set; }

    /// <summary>Whether <see cref="GoForwardAsync"/> was called.</summary>
    public bool GoForwardCalled { get; private set; }

    /// <summary>Whether <see cref="ReloadAsync"/> was called.</summary>
    public bool ReloadCalled { get; private set; }

    /// <summary>The number of times <see cref="BringToFrontAsync"/> was called.</summary>
    public int BringToFrontCount { get; private set; }

    /// <summary>Whether <see cref="CloseAsync"/> was called.</summary>
    public bool CloseCalled { get; private set; }

    /// <summary>The last expression passed to <see cref="EvaluateAsync{T}"/>.</summary>
    public string? EvaluatedExpression { get; private set; }

    /// <summary>The value <see cref="EvaluateAsync{T}"/> returns when asked for a <see cref="JsonElement"/>.</summary>
    public JsonElement EvaluateReturn { get; set; }

    /// <summary>When set, <see cref="EvaluateAsync{T}"/> throws this to simulate a script error.</summary>
    public Exception? EvaluateError { get; init; }

    /// <summary>The result <see cref="RunAccessibilityAuditAsync"/> returns. Defaults to an empty audit.</summary>
    public AccessibilityAuditResult AuditResult { get; set; } =
        new([], PassCount: 0, ViolationCount: 0, Duration: TimeSpan.Zero);

    /// <summary>When set, <see cref="RunAccessibilityAuditAsync"/> throws this to simulate an audit failure.</summary>
    public Exception? AuditError { get; init; }

    /// <summary>The metrics <see cref="GetPerformanceMetricsAsync"/> returns. Null models no collection yet.</summary>
    public PerformanceMetrics? PerformanceMetrics { get; set; }

    /// <summary>When set, <see cref="GetPerformanceMetricsAsync"/> throws this to simulate a collection failure.</summary>
    public Exception? PerformanceError { get; init; }

    /// <summary>Whether a HAR recording is currently in progress on this fake.</summary>
    public bool HarRecording { get; private set; }

    /// <summary>The path passed to the most recent <see cref="StopHarRecordingAsync"/> call.</summary>
    public string? HarStoppedPath { get; private set; }

    /// <summary>When set, <see cref="StopHarRecordingAsync"/> throws this to simulate a write failure.</summary>
    public Exception? HarError { get; init; }

    /// <summary>The path the in-progress fake video recording writes to; null when not recording.</summary>
    public string? VideoRecordingPath { get; private set; }

    /// <summary>The path returned by the most recent <see cref="StopVideoRecordingAsync"/> call.</summary>
    public string? VideoStoppedPath { get; private set; }

    /// <summary>When set, <see cref="StartVideoRecordingAsync"/> throws this, e.g. to model an already-recording page.</summary>
    public Exception? VideoStartError { get; init; }

    /// <summary>When set, <see cref="StopVideoRecordingAsync"/> throws this, e.g. to model no recording in progress.</summary>
    public Exception? VideoStopError { get; init; }

    private bool _closed;

    /// <summary>Raises the <see cref="Dialog"/> event with the given dialog, as the browser would.</summary>
    public void RaiseDialog(IDialog dialog) => Dialog?.Invoke(this, new DialogEventArgs(dialog));

    /// <summary>Raises the <see cref="Console"/> event, as the browser would on a console call.</summary>
    public void RaiseConsole(string type, string text)
        => Console?.Invoke(this, new ConsoleMessageEventArgs(type, text));

    /// <summary>Raises the <see cref="PageError"/> event, as the browser would on an uncaught error.</summary>
    public void RaisePageError(string message, string? stack = null)
        => PageError?.Invoke(this, new PageErrorEventArgs(message, stack));

    /// <summary>Raises the <see cref="Response"/> event, as the browser would when a response arrives.</summary>
    public void RaiseResponse(IResponse response) => Response?.Invoke(this, new ResponseEventArgs(response));

    /// <summary>Raises the <see cref="RequestFailed"/> event, as the browser would on a failed request.</summary>
    public void RaiseRequestFailed(IRequest request) => RequestFailed?.Invoke(this, new RequestEventArgs(request));

    public Task<AccessibilitySnapshot> AccessibilitySnapshotAsync(CancellationToken ct = default)
        => Task.FromResult(snapshot);

    public Task<AccessibilityAuditResult> RunAccessibilityAuditAsync(CancellationToken ct = default)
    {
        if (AuditError is not null)
            throw AuditError;
        return Task.FromResult(AuditResult);
    }

    public Task<PerformanceMetrics?> GetPerformanceMetricsAsync(CancellationToken ct = default)
    {
        if (PerformanceError is not null)
            throw PerformanceError;
        return Task.FromResult(PerformanceMetrics);
    }

    public Task StartHarRecordingAsync(CancellationToken ct = default)
    {
        HarRecording = true;
        return Task.CompletedTask;
    }

    public Task StopHarRecordingAsync(string path, CancellationToken ct = default)
    {
        if (HarError is not null)
            throw HarError;
        HarRecording = false;
        HarStoppedPath = path;
        return Task.CompletedTask;
    }

    public Task<string> StartVideoRecordingAsync(string? path = null, ViewportSize? size = null, CancellationToken ct = default)
    {
        if (VideoStartError is not null)
            throw VideoStartError;
        VideoRecordingPath = path ?? Path.Combine(Path.GetTempPath(), "fake-video.avi");
        return Task.FromResult(VideoRecordingPath);
    }

    public Task<string> StopVideoRecordingAsync(CancellationToken ct = default)
    {
        if (VideoStopError is not null)
            throw VideoStopError;
        if (VideoRecordingPath is null)
            throw new InvalidOperationException("No video recording is in progress on this page.");
        VideoStoppedPath = VideoRecordingPath;
        VideoRecordingPath = null;
        return Task.FromResult(VideoStoppedPath);
    }

    public ILocator LocatorByBackendNodeId(long backendNodeId)
    {
        ResolvedBackendNodeId = backendNodeId;
        return RecordingLocator;
    }

    public Task<IResponse?> GotoAsync(string url, NavigationOptions? options = null)
    {
        if (GotoError is not null)
            throw GotoError;

        NavigatedUrl = url;
        return Task.FromResult<IResponse?>(null);
    }

    public Task<byte[]> ScreenshotAsync(ScreenshotOptions? options = null)
    {
        ScreenshotFullPage = options?.FullPage;
        return Task.FromResult(new byte[] { 0x89, 0x50, 0x4E, 0x47 });
    }

    public bool IsClosed => _closed;

    public ValueTask DisposeAsync() => ValueTask.CompletedTask;

#pragma warning disable CS0067 // events are part of the interface but unused in tests
    public event EventHandler? Close;
    public event EventHandler<ConsoleMessageEventArgs>? Console;
    public event EventHandler<DialogEventArgs>? Dialog;
    public event EventHandler<IDownload>? Download;
    public event EventHandler<IFileChooser>? FileChooser;
    public event EventHandler<PageErrorEventArgs>? PageError;
    public event EventHandler<IPage>? Popup;
    public event EventHandler<RequestEventArgs>? Request;
    public event EventHandler<RequestEventArgs>? RequestFailed;
    public event EventHandler<RequestEventArgs>? RequestFinished;
    public event EventHandler<ResponseEventArgs>? Response;
    public event EventHandler? Load;
    public event EventHandler? DOMContentLoaded;
#pragma warning restore CS0067

    public IBrowserContext Context => throw new NotImplementedException();
    public IFrame MainFrame => throw new NotImplementedException();
    public IReadOnlyList<IFrame> Frames => throw new NotImplementedException();
    public string Url => PageUrl;
    public IKeyboard Keyboard => FakeKeyboard;
    public IMouse Mouse => FakeMouse;
    public ITouchscreen Touchscreen => throw new NotImplementedException();
    public IVideo? Video => throw new NotImplementedException();
    public ViewportSize? ViewportSize => throw new NotImplementedException();

    public Task<IResponse?> GoBackAsync(NavigationOptions? options = null)
    {
        GoBackCalled = true;
        return Task.FromResult(GoBackResponse);
    }
    public Task<IResponse?> GoForwardAsync(NavigationOptions? options = null)
    {
        GoForwardCalled = true;
        return Task.FromResult(GoForwardResponse);
    }
    public Task<IResponse?> ReloadAsync(NavigationOptions? options = null)
    {
        ReloadCalled = true;
        return Task.FromResult<IResponse?>(null);
    }
    public Task<string> ContentAsync() => throw new NotImplementedException();
    public Task SetContentAsync(string html, NavigationOptions? options = null) => throw new NotImplementedException();
    public Task<string> TitleAsync() => Task.FromResult(PageTitle);
    public ILocator Locator(string selector, LocatorOptions? options = null) => throw new NotImplementedException();
    public ILocator GetByRole(string role, string? name = null) => throw new NotImplementedException();
    public ILocator GetByText(string text, bool? exact = null) => throw new NotImplementedException();
    public ILocator GetByLabel(string text, bool? exact = null) => throw new NotImplementedException();
    public ILocator GetByPlaceholder(string text, bool? exact = null) => throw new NotImplementedException();
    public ILocator GetByTestId(string testId) => throw new NotImplementedException();
    public ILocator GetByTitle(string text, bool? exact = null) => throw new NotImplementedException();
    public ILocator GetByAltText(string text, bool? exact = null) => throw new NotImplementedException();
    public Task<T> EvaluateAsync<T>(string expression, object? arg = null)
    {
        EvaluatedExpression = expression;
        if (EvaluateError is not null)
            throw EvaluateError;
        if (typeof(T) == typeof(JsonElement))
            return Task.FromResult((T)(object)EvaluateReturn);
        throw new NotImplementedException();
    }
    public Task<IJSHandle> EvaluateHandleAsync(string expression, object? arg = null) => throw new NotImplementedException();
    public Task<T> WaitForFunctionAsync<T>(string expression, object? arg = null, double? timeout = null)
    {
        WaitedFunctions.Add(expression);
        WaitedFunctionArgs.Add(arg);
        return Task.FromResult<T>(default!);
    }

    public Task WaitForLoadStateAsync(LoadState? state = null, double? timeout = null) => throw new NotImplementedException();
    public Task<IResponse?> WaitForNavigationAsync(NavigationOptions? options = null) => throw new NotImplementedException();
    public Task WaitForURLAsync(string urlPattern, NavigationOptions? options = null) => throw new NotImplementedException();
    public Task<IRequest> WaitForRequestAsync(string urlPattern, double? timeout = null) => throw new NotImplementedException();
    public Task<IResponse> WaitForResponseAsync(string urlPattern, double? timeout = null) => throw new NotImplementedException();
    public Task<IRequest> WaitForRequestAsync(Func<IRequest, bool> predicate, double? timeout = null) => throw new NotImplementedException();
    public Task<IResponse> WaitForResponseAsync(Func<IResponse, bool> predicate, double? timeout = null) => throw new NotImplementedException();
    public Task<IPage> WaitForPopupAsync(double? timeout = null) => throw new NotImplementedException();
    public Task WaitForTimeoutAsync(double timeout)
    {
        WaitedTimeoutMs = timeout;
        return Task.CompletedTask;
    }
    public Task RouteAsync(string urlPattern, Func<IRoute, Task> handler) => throw new NotImplementedException();
    public Task UnrouteAsync(string urlPattern, Func<IRoute, Task>? handler = null) => throw new NotImplementedException();
    public Task AddInitScriptAsync(string script) => throw new NotImplementedException();
    public Task SetViewportSizeAsync(ViewportSize viewportSize)
    {
        ResizedTo = viewportSize;
        return Task.CompletedTask;
    }
    public Task<IElementHandle> AddScriptTagAsync(string? url = null, string? content = null) => throw new NotImplementedException();
    public Task<IElementHandle> AddStyleTagAsync(string? url = null, string? content = null) => throw new NotImplementedException();
    public Task ExposeBindingAsync(string name, Func<object?[], Task<object?>> callback) => throw new NotImplementedException();
    public Task CloseAsync(bool? runBeforeUnload = null)
    {
        CloseCalled = true;
        _closed = true;
        return Task.CompletedTask;
    }
    public Task BringToFrontAsync()
    {
        BringToFrontCount++;
        return Task.CompletedTask;
    }
    public Task PauseAsync() => throw new NotImplementedException();
    public Task<byte[]> PdfAsync(string? path = null) => throw new NotImplementedException();
    public Task EmulateMediaAsync(string? media = null, ColorScheme? colorScheme = null) => throw new NotImplementedException();
}

/// <summary>
/// A fake locator that records the actions the tools invoke and no-ops the rest.
/// </summary>
internal sealed class FakeToolLocator : ILocator
{
    public int ClickCount { get; private set; }
    public int DblClickCount { get; private set; }
    public string? FilledValue { get; private set; }
    public string? TypedValue { get; private set; }
    public List<string> PressedKeys { get; } = [];
    public int HoverCount { get; private set; }
    public int ClearCount { get; private set; }
    public int FocusCount { get; private set; }
    public int ScrollIntoViewCount { get; private set; }
    public bool? CheckedValue { get; private set; }
    public IReadOnlyList<string>? SelectedValues { get; private set; }
    public IReadOnlyList<FilePayload>? UploadedFiles { get; private set; }
    public ElementState? WaitedForState { get; private set; }
    public string? EvaluatedElementExpression { get; private set; }
    public JsonElement ElementEvaluateReturn { get; set; }

    public Task ClickAsync(double? timeout = null)
    {
        ClickCount++;
        return Task.CompletedTask;
    }

    public Task DblClickAsync(double? timeout = null)
    {
        DblClickCount++;
        return Task.CompletedTask;
    }

    public Task FillAsync(string value, double? timeout = null)
    {
        FilledValue = value;
        return Task.CompletedTask;
    }

    public Task TypeAsync(string text, KeyboardTypeOptions? options = null)
    {
        TypedValue = text;
        return Task.CompletedTask;
    }

    public Task PressAsync(string key, KeyboardPressOptions? options = null)
    {
        PressedKeys.Add(key);
        return Task.CompletedTask;
    }

    public Task CheckAsync(double? timeout = null) => throw new NotImplementedException();
    public Task UncheckAsync(double? timeout = null) => throw new NotImplementedException();

    public Task SetCheckedAsync(bool @checked, double? timeout = null)
    {
        CheckedValue = @checked;
        return Task.CompletedTask;
    }

    public Task ClearAsync(double? timeout = null)
    {
        ClearCount++;
        return Task.CompletedTask;
    }

    public Task FocusAsync(double? timeout = null)
    {
        FocusCount++;
        return Task.CompletedTask;
    }

    public Task HoverAsync(double? timeout = null)
    {
        HoverCount++;
        return Task.CompletedTask;
    }

    public Task<IReadOnlyList<string>> SelectOptionAsync(params string[] values)
    {
        SelectedValues = values;
        return Task.FromResult<IReadOnlyList<string>>(values);
    }

    public Task SetInputFilesAsync(IEnumerable<FilePayload> files, double? timeout = null)
    {
        UploadedFiles = files.ToList();
        return Task.CompletedTask;
    }

    public Task TapAsync(double? timeout = null) => throw new NotImplementedException();

    public Task ScrollIntoViewIfNeededAsync(double? timeout = null)
    {
        ScrollIntoViewCount++;
        return Task.CompletedTask;
    }
    public Task<byte[]> ScreenshotAsync(ScreenshotOptions? options = null) => throw new NotImplementedException();
    public Task DispatchEventAsync(string type, object? eventInit = null) => throw new NotImplementedException();
    public Task<T> EvaluateAsync<T>(string expression, object? arg = null) => throw new NotImplementedException();
    public Task<T> EvaluateWithElementAsync<T>(string pageFunction, object? arg = null)
    {
        EvaluatedElementExpression = pageFunction;
        if (typeof(T) == typeof(JsonElement))
            return Task.FromResult((T)(object)ElementEvaluateReturn);
        throw new NotImplementedException();
    }
    public Task<string?> TextContentAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<string> InnerTextAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<string> InnerHTMLAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<string?> GetAttributeAsync(string name, double? timeout = null) => throw new NotImplementedException();
    public Task<string> InputValueAsync(double? timeout = null) => throw new NotImplementedException();
    /// <summary>The box <see cref="BoundingBoxAsync"/> returns; tests can override it.</summary>
    public BoundingBox? Box { get; set; } = new(100, 200, 50, 30);

    public Task<BoundingBox?> BoundingBoxAsync(double? timeout = null) => Task.FromResult(Box);
    public Task<int> CountAsync() => throw new NotImplementedException();
    public Task<IReadOnlyList<string>> AllInnerTextsAsync() => throw new NotImplementedException();
    public Task<IReadOnlyList<string>> AllTextContentsAsync() => throw new NotImplementedException();
    public Task<bool> IsCheckedAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<bool> IsDisabledAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<bool> IsEditableAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<bool> IsEnabledAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<bool> IsHiddenAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<bool> IsVisibleAsync(double? timeout = null) => throw new NotImplementedException();
    public ILocator First => throw new NotImplementedException();
    public ILocator Last => throw new NotImplementedException();
    public ILocator Nth(int index) => throw new NotImplementedException();
    public ILocator Filter(LocatorOptions? options = null) => throw new NotImplementedException();
    public ILocator Locator(string selector, LocatorOptions? options = null) => throw new NotImplementedException();
    public Task WaitForAsync(ElementState? state = null, double? timeout = null)
    {
        WaitedForState = state;
        return Task.CompletedTask;
    }
    public Task<IElementHandle> ElementHandleAsync(double? timeout = null) => throw new NotImplementedException();
    public Task<IReadOnlyList<IElementHandle>> ElementHandlesAsync() => throw new NotImplementedException();
}

/// <summary>
/// A fake page-level keyboard that records the keys <c>press_key</c> sends and
/// no-ops the rest.
/// </summary>
/// <summary>
/// A fake mouse that records every event the coordinate tools dispatch, in order,
/// so tests can assert both the calls and their sequence.
/// </summary>
internal sealed class FakeMouse : IMouse
{
    /// <summary>Every mouse call in dispatch order: "move", "down", "up", "click", "dblclick", "wheel".</summary>
    public List<string> Sequence { get; } = [];

    public List<(double X, double Y, MouseMoveOptions? Options)> Moves { get; } = [];
    public List<MouseButtonOptions?> Downs { get; } = [];
    public List<MouseButtonOptions?> Ups { get; } = [];
    public List<(double X, double Y, MouseButtonOptions? Options)> Clicks { get; } = [];
    public List<(double X, double Y, MouseButtonOptions? Options)> DblClicks { get; } = [];
    public List<(double DeltaX, double DeltaY)> Wheels { get; } = [];

    public Task MoveAsync(double x, double y, MouseMoveOptions? options = null)
    {
        Sequence.Add("move");
        Moves.Add((x, y, options));
        return Task.CompletedTask;
    }

    public Task DownAsync(MouseButtonOptions? options = null)
    {
        Sequence.Add("down");
        Downs.Add(options);
        return Task.CompletedTask;
    }

    public Task UpAsync(MouseButtonOptions? options = null)
    {
        Sequence.Add("up");
        Ups.Add(options);
        return Task.CompletedTask;
    }

    public Task ClickAsync(double x, double y, MouseButtonOptions? options = null)
    {
        Sequence.Add("click");
        Clicks.Add((x, y, options));
        return Task.CompletedTask;
    }

    public Task DblClickAsync(double x, double y, MouseButtonOptions? options = null)
    {
        Sequence.Add("dblclick");
        DblClicks.Add((x, y, options));
        return Task.CompletedTask;
    }

    public Task WheelAsync(double deltaX, double deltaY)
    {
        Sequence.Add("wheel");
        Wheels.Add((deltaX, deltaY));
        return Task.CompletedTask;
    }
}

internal sealed class FakeKeyboard : IKeyboard
{
    public List<string> PressedKeys { get; } = [];

    public Task PressAsync(string key, KeyboardPressOptions? options = null)
    {
        PressedKeys.Add(key);
        return Task.CompletedTask;
    }

    public Task DownAsync(string key) => throw new NotImplementedException();
    public Task UpAsync(string key) => throw new NotImplementedException();
    public Task TypeAsync(string text, KeyboardTypeOptions? options = null) => throw new NotImplementedException();
    public Task InsertTextAsync(string text) => throw new NotImplementedException();
}

/// <summary>
/// A fake dialog that records whether it was accepted (with what text) or dismissed.
/// </summary>
internal sealed class FakeDialog(DialogType type = DialogType.Alert, string message = "", string? defaultValue = null)
    : IDialog
{
    public DialogType Type { get; } = type;
    public string Message { get; } = message;
    public string? DefaultValue { get; } = defaultValue;
    public bool Accepted { get; private set; }
    public bool Dismissed { get; private set; }
    public string? AcceptedText { get; private set; }

    public Task AcceptAsync(string? promptText = null)
    {
        Accepted = true;
        AcceptedText = promptText;
        return Task.CompletedTask;
    }

    public Task DismissAsync()
    {
        Dismissed = true;
        return Task.CompletedTask;
    }
}

/// <summary>
/// An <see cref="ActivePageService"/> for the network tools. It serves a fixed fake
/// page and returns a recording <see cref="FakeBrowserContext"/> as the active
/// context, so route registration runs for real against the fake context without a
/// browser.
/// </summary>
internal sealed class FakeNetworkPageService : ActivePageService
{
    private readonly FakeToolPage _page;

    public FakeNetworkPageService(FakeToolPage? page = null)
        : base(new BrowserSessionManager(new McpServerLaunchOptions()))
        => _page = page ?? new FakeToolPage(new AccessibilitySnapshot([], 0, null));

    /// <summary>The recording context returned by <see cref="GetOrCreateActiveContextAsync"/>.</summary>
    public FakeBrowserContext Context { get; } = new();

    protected override Task<IPage> ResolvePageAsync(CancellationToken cancellationToken)
        => Task.FromResult<IPage>(_page);

    public override Task<IBrowserContext> GetOrCreateActiveContextAsync(CancellationToken cancellationToken = default)
        => Task.FromResult<IBrowserContext>(Context);
}

/// <summary>
/// A fake context that records the route patterns registered and removed and keeps
/// the handler for each pattern so a test can invoke it. Every other member is unused.
/// </summary>
internal sealed class FakeBrowserContext : IBrowserContext
{
    /// <summary>The patterns passed to <see cref="RouteAsync"/>, in call order.</summary>
    public List<string> RoutedPatterns { get; } = [];

    /// <summary>The patterns passed to <see cref="UnrouteAsync"/>, in call order.</summary>
    public List<string> UnroutedPatterns { get; } = [];

    /// <summary>The handler registered for each routed pattern.</summary>
    public Dictionary<string, Func<IRoute, Task>> Handlers { get; } = new(StringComparer.Ordinal);

    /// <summary>The recording tracing fake, so a test can assert on start/stop calls.</summary>
    public FakeTracing TracingFake { get; } = new();

    public Task RouteAsync(string urlPattern, Func<IRoute, Task> handler)
    {
        RoutedPatterns.Add(urlPattern);
        Handlers[urlPattern] = handler;
        return Task.CompletedTask;
    }

    public Task UnrouteAsync(string urlPattern, Func<IRoute, Task>? handler = null)
    {
        UnroutedPatterns.Add(urlPattern);
        Handlers.Remove(urlPattern);
        return Task.CompletedTask;
    }

    public ValueTask DisposeAsync() => ValueTask.CompletedTask;

#pragma warning disable CS0067 // events are part of the interface but unused in tests
    public event EventHandler<IPage>? Page;
    public event EventHandler? Close;
#pragma warning restore CS0067

    public IBrowser Browser => throw new NotImplementedException();
    public IReadOnlyList<IPage> Pages => throw new NotImplementedException();
    public ITracing Tracing => TracingFake;
    public Task<IPage> NewPageAsync() => throw new NotImplementedException();
    public Task<IReadOnlyList<Cookie>> CookiesAsync(IEnumerable<string>? urls = null) => throw new NotImplementedException();
    public Task AddCookiesAsync(IEnumerable<Cookie> cookies) => throw new NotImplementedException();
    public Task ClearCookiesAsync() => throw new NotImplementedException();
    public Task GrantPermissionsAsync(IEnumerable<string> permissions, string? origin = null) => throw new NotImplementedException();
    public Task ClearPermissionsAsync() => throw new NotImplementedException();
    public Task SetGeolocationAsync(Geolocation? geolocation) => throw new NotImplementedException();
    public Task SetOfflineAsync(bool offline) => throw new NotImplementedException();
    public Task SetExtraHTTPHeadersAsync(IDictionary<string, string> headers) => throw new NotImplementedException();
    public Task<StorageState> StorageStateAsync(string? path = null) => throw new NotImplementedException();
    public Task ExposeBindingAsync(string name, Func<object?[], Task<object?>> callback) => throw new NotImplementedException();
    public Task AddInitScriptAsync(string script) => throw new NotImplementedException();
    public IPluginContext GetPluginContext() => throw new NotImplementedException();
    public Task CloseAsync() => throw new NotImplementedException();
}

/// <summary>A fake tracing engine that records the options it was started and stopped with.</summary>
internal sealed class FakeTracing : ITracing
{
    public bool Started { get; private set; }
    public TracingStartOptions? StartedWith { get; private set; }
    public bool Stopped { get; private set; }
    public TracingStopOptions? StoppedWith { get; private set; }

    public Task StartAsync(TracingStartOptions? options = null)
    {
        Started = true;
        StartedWith = options;
        return Task.CompletedTask;
    }

    public Task StopAsync(TracingStopOptions? options = null)
    {
        Stopped = true;
        StoppedWith = options;
        return Task.CompletedTask;
    }
}

/// <summary>
/// A fake route that records how it was answered: fulfilled (with what options),
/// continued (with what options), or aborted (with what error code).
/// </summary>
internal sealed class FakeRoute(IRequest? request = null) : IRoute
{
    public IRequest Request { get; } = request ?? new FakeRequest();

    public RouteFulfillOptions? FulfilledWith { get; private set; }
    public RouteContinueOptions? ContinuedWith { get; private set; }
    public bool ContinueCalled { get; private set; }
    public string? AbortedWith { get; private set; }
    public bool AbortCalled { get; private set; }

    public Task FulfillAsync(RouteFulfillOptions? options = null)
    {
        FulfilledWith = options;
        return Task.CompletedTask;
    }

    public Task ContinueAsync(RouteContinueOptions? options = null)
    {
        ContinueCalled = true;
        ContinuedWith = options;
        return Task.CompletedTask;
    }

    public Task AbortAsync(string? errorCode = null)
    {
        AbortCalled = true;
        AbortedWith = errorCode;
        return Task.CompletedTask;
    }
}

/// <summary>A fake request exposing the fields the request log reads. Every other member is unused.</summary>
internal sealed class FakeRequest(string method = "GET", string url = "https://example.test/", string resourceType = "fetch")
    : IRequest
{
    public string Url { get; } = url;
    public string Method { get; } = method;
    public string ResourceType { get; } = resourceType;
    public string? PostData => null;
    public bool IsNavigationRequest => false;
    public IResponse? Response => null;
    public IHeaderCollection Headers => throw new NotImplementedException();
    public IFrame Frame => throw new NotImplementedException();
}

/// <summary>A fake response exposing the fields the request log reads. Every other member is unused.</summary>
internal sealed class FakeResponse(IRequest request, int status = 200, string? url = null) : IResponse
{
    public string Url { get; } = url ?? request.Url;
    public int Status { get; } = status;
    public IRequest Request { get; } = request;
    public string StatusText => "OK";
    public bool Ok => Status is >= 200 and <= 299;
    public IHeaderCollection Headers => throw new NotImplementedException();
    public IFrame Frame => throw new NotImplementedException();
    public Task<byte[]> BodyAsync(CancellationToken ct = default) => throw new NotImplementedException();
    public Task<string> TextAsync(CancellationToken ct = default) => throw new NotImplementedException();
    public Task<T> JsonAsync<T>(CancellationToken ct = default) => throw new NotImplementedException();
}

/// <summary>
/// An <see cref="ActivePageService"/> for the tab and context tools. It overrides the
/// browser-touching seams to work over an in-memory list of fake tabs and a simulated
/// set of context names, so the tools' own index validation, active-page tracking, and
/// error mapping run for real without a browser.
/// </summary>
internal sealed class FakeSessionPageService : ActivePageService
{
    private readonly List<FakeToolPage> _pages;

    public FakeSessionPageService(params FakeToolPage[] pages)
        : base(new BrowserSessionManager(new McpServerLaunchOptions()))
    {
        _pages = pages.Length == 0 ? [NewPage()] : [.. pages];
    }

    /// <summary>The simulated open context names; the first is the implicit default.</summary>
    public List<string> Contexts { get; } = [BrowserSessionManager.DefaultContextName];

    /// <summary>The simulated active context name.</summary>
    public string ActiveContext { get; private set; } = BrowserSessionManager.DefaultContextName;

    public List<string> CreatedContexts { get; } = [];
    public List<string> SelectedContexts { get; } = [];
    public List<string> ClosedContexts { get; } = [];
    public int OpenedTabs { get; private set; }

    public IReadOnlyList<FakeToolPage> Tabs => _pages;

    private static FakeToolPage NewPage() => new(new AccessibilitySnapshot([], 0, null));

    protected override Task<IPage> ResolvePageAsync(CancellationToken cancellationToken)
    {
        var open = _pages.FirstOrDefault(p => !p.IsClosed) ?? AddTab();
        return Task.FromResult<IPage>(open);
    }

    protected override Task<IReadOnlyList<IPage>> GetActiveContextPagesAsync(CancellationToken cancellationToken)
        => Task.FromResult<IReadOnlyList<IPage>>(_pages.Where(p => !p.IsClosed).Cast<IPage>().ToArray());

    public override Task<IPage> OpenNewTabAsync(CancellationToken cancellationToken = default)
    {
        OpenedTabs++;
        var page = AddTab();
        SelectPage(page);
        return Task.FromResult<IPage>(page);
    }

    public override Task CreateContextAsync(string name, CancellationToken cancellationToken = default)
    {
        if (Contexts.Contains(name))
            throw new InvalidOperationException($"A context named '{name}' already exists.");

        Contexts.Add(name);
        ActiveContext = name;
        CreatedContexts.Add(name);
        ResetActivePage();
        return Task.CompletedTask;
    }

    public override void SelectContext(string name)
    {
        if (!Contexts.Contains(name))
            throw new InvalidOperationException($"No open context named '{name}'.");

        ActiveContext = name;
        SelectedContexts.Add(name);
        ResetActivePage();
    }

    public override Task CloseContextAsync(string name, CancellationToken cancellationToken = default)
    {
        Contexts.Remove(name);
        if (ActiveContext == name)
            ActiveContext = BrowserSessionManager.DefaultContextName;

        ClosedContexts.Add(name);
        ResetActivePage();
        return Task.CompletedTask;
    }

    public override IReadOnlyCollection<string> GetContextNames() => Contexts;

    public override string GetActiveContextName() => ActiveContext;

    private FakeToolPage AddTab()
    {
        var page = NewPage();
        _pages.Add(page);
        return page;
    }
}
