namespace Armada.Test.Automated.Suites
{
    using System;
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Net.WebSockets;
    using System.Text;
    using System.Text.Json;
    using System.Threading;
    using System.Threading.Tasks;
    using Armada.Core.Enums;
    using Armada.Core.Models;
    using Armada.Test.Common;

    /// <summary>
    /// WebSocket route tests migrated from xUnit to TestSuite harness.
    /// </summary>
    public class WebSocketTests : TestSuite
    {
        #region Public-Members

        /// <summary>
        /// Name of this test suite.
        /// </summary>
        public override string Name => "WebSocket Tests";

        #endregion

        #region Private-Members

        private HttpClient _AuthClient;
        private HttpClient _UnauthClient;
        private int _RestPort;
        private string _ApiKey;

        #endregion

        #region Constructors-and-Factories

        /// <summary>
        /// Create a new WebSocket test suite.
        /// </summary>
        public WebSocketTests(HttpClient authClient, HttpClient unauthClient, int restPort, string apiKey)
        {
            _AuthClient = authClient ?? throw new ArgumentNullException(nameof(authClient));
            _UnauthClient = unauthClient ?? throw new ArgumentNullException(nameof(unauthClient));
            _RestPort = restPort;
            _ApiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
        }

        #endregion

        #region Public-Methods

        /// <summary>
        /// Run all WebSocket tests.
        /// </summary>
        protected override async Task RunTestsAsync()
        {
            // Subscribe Tests
            await RunTest("Subscribe_ReturnsStatusSnapshot", async () =>
            {
                using ClientWebSocket ws = await ConnectAsync().ConfigureAwait(false);

                string msg = JsonHelper.Serialize(new { Route = "subscribe" });
                byte[] bytes = Encoding.UTF8.GetBytes(msg);
                await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);

                byte[] buffer = new byte[1048576];
                using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
                WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token).ConfigureAwait(false);
                string json = Encoding.UTF8.GetString(buffer, 0, result.Count);
                using JsonDocument doc = JsonDocument.Parse(json);
                JsonElement root = doc.RootElement;

                AssertEqual("status.snapshot", root.GetProperty("type").GetString());
                Assert(root.TryGetProperty("data", out _), "Should contain data property");
                Assert(root.TryGetProperty("timestamp", out _), "Should contain timestamp");
            }).ConfigureAwait(false);

            await RunTest("Subscribe_CheckRunImport_BroadcastsCheckRunChanged", async () =>
            {
                using ClientWebSocket ws = await ConnectAsync().ConfigureAwait(false);
                await SubscribeAsync(ws).ConfigureAwait(false);

                string vesselId = await CreateVesselViaRestAsync("ws-checkrun-broadcast").ConfigureAwait(false);
                HttpResponseMessage response = await _AuthClient.PostAsync("/api/v1/check-runs/import",
                    JsonHelper.ToJsonContent(new
                    {
                        VesselId = vesselId,
                        Type = "Build",
                        Status = "Passed",
                        Label = "WebSocket Imported Check",
                        Command = "echo imported",
                        Output = "Build succeeded.",
                        Summary = "Imported check passed."
                    })).ConfigureAwait(false);
                response.EnsureSuccessStatusCode();
                CheckRun created = await JsonHelper.DeserializeAsync<CheckRun>(response).ConfigureAwait(false);

                JsonElement evt = await WaitForEventAsync(ws, root =>
                {
                    return root.GetProperty("type").GetString() == "check-run.changed"
                        && root.GetProperty("data").GetProperty("id").GetString() == created.Id;
                }).ConfigureAwait(false);

                AssertEqual("check-run.changed", evt.GetProperty("type").GetString());
                AssertEqual(created.Id, evt.GetProperty("data").GetProperty("id").GetString());
                AssertEqual("Passed", evt.GetProperty("data").GetProperty("status").GetString());
            }).ConfigureAwait(false);

            await RunTest("Subscribe_MissionReviewTransition_BroadcastsApprovalNeeded", async () =>
            {
                using ClientWebSocket ws = await ConnectAsync().ConfigureAwait(false);
                await SubscribeAsync(ws).ConfigureAwait(false);

                string missionId = await CreateMissionViaRestAsync("ws-approval-needed").ConfigureAwait(false);

                HttpResponseMessage assignedResponse = await _AuthClient.PutAsync(
                    "/api/v1/missions/" + missionId + "/status",
                    JsonHelper.ToJsonContent(new { Status = "Assigned" })).ConfigureAwait(false);
                assignedResponse.EnsureSuccessStatusCode();

                HttpResponseMessage inProgressResponse = await _AuthClient.PutAsync(
                    "/api/v1/missions/" + missionId + "/status",
                    JsonHelper.ToJsonContent(new { Status = "InProgress" })).ConfigureAwait(false);
                inProgressResponse.EnsureSuccessStatusCode();

                HttpResponseMessage reviewResponse = await _AuthClient.PutAsync(
                    "/api/v1/missions/" + missionId + "/status",
                    JsonHelper.ToJsonContent(new { Status = "Review" })).ConfigureAwait(false);
                reviewResponse.EnsureSuccessStatusCode();

                JsonElement evt = await WaitForEventAsync(ws, root =>
                {
                    return root.GetProperty("type").GetString() == "approval-needed"
                        && root.GetProperty("data").GetProperty("missionId").GetString() == missionId;
                }).ConfigureAwait(false);

                AssertEqual("approval-needed", evt.GetProperty("type").GetString());
                AssertEqual(missionId, evt.GetProperty("data").GetProperty("missionId").GetString());
                AssertEqual("mission", evt.GetProperty("data").GetProperty("entityType").GetString());
                AssertEqual("Review", evt.GetProperty("data").GetProperty("status").GetString());
            }).ConfigureAwait(false);

            await RunTest("Subscribe_BacklogCreate_BroadcastsObjectiveChanged", async () =>
            {
                string objectiveId = String.Empty;

                try
                {
                    using ClientWebSocket ws = await ConnectAsync().ConfigureAwait(false);
                    await SubscribeAsync(ws).ConfigureAwait(false);

                    HttpResponseMessage createResponse = await _AuthClient.PostAsync(
                        "/api/v1/backlog",
                        JsonHelper.ToJsonContent(new
                        {
                            Title = "WebSocket backlog coverage",
                            Description = "Broadcast objective.changed for backlog routes."
                        })).ConfigureAwait(false);
                    createResponse.EnsureSuccessStatusCode();
                    Objective created = await JsonHelper.DeserializeAsync<Objective>(createResponse).ConfigureAwait(false);
                    objectiveId = created.Id;

                    JsonElement evt = await WaitForEventAsync(ws, root =>
                    {
                        return root.GetProperty("type").GetString() == "objective.changed"
                            && root.GetProperty("data").GetProperty("id").GetString() == objectiveId;
                    }).ConfigureAwait(false);

                    AssertEqual("objective.changed", evt.GetProperty("type").GetString());
                    AssertEqual(objectiveId, evt.GetProperty("data").GetProperty("id").GetString());
                    AssertEqual("Inbox", evt.GetProperty("data").GetProperty("backlogState").GetString());
                }
                finally
                {
                    if (!String.IsNullOrWhiteSpace(objectiveId))
                        await _AuthClient.DeleteAsync("/api/v1/backlog/" + objectiveId).ConfigureAwait(false);
                }
            }).ConfigureAwait(false);

            await RunTest("Subscribe_BacklogRefinementLifecycle_BroadcastsRefinementEvents", async () =>
            {
                string objectiveId = String.Empty;
                string captainId = String.Empty;
                string sessionId = String.Empty;

                try
                {
                    using ClientWebSocket ws = await ConnectAsync().ConfigureAwait(false);
                    await SubscribeAsync(ws).ConfigureAwait(false);

                    Objective objective = await CreateObjectiveViaRestAsync("WebSocket refinement objective").ConfigureAwait(false);
                    objectiveId = objective.Id;

                    Captain captain = await CreateCaptainRecordViaRestAsync("ws-refinement-captain").ConfigureAwait(false);
                    captainId = captain.Id;

                    HttpResponseMessage createSessionResponse = await _AuthClient.PostAsync(
                        "/api/v1/backlog/" + objectiveId + "/refinement-sessions",
                        JsonHelper.ToJsonContent(new
                        {
                            CaptainId = captainId,
                            Title = "WebSocket refinement lifecycle"
                        })).ConfigureAwait(false);
                    createSessionResponse.EnsureSuccessStatusCode();
                    ObjectiveRefinementSessionDetail createdSession = await JsonHelper.DeserializeAsync<ObjectiveRefinementSessionDetail>(createSessionResponse).ConfigureAwait(false);
                    sessionId = createdSession.Session.Id;

                    JsonElement createdEvent = await WaitForEventAsync(ws, root =>
                    {
                        return root.GetProperty("type").GetString() == "objective-refinement-session.changed"
                            && root.GetProperty("data").GetProperty("session").GetProperty("id").GetString() == sessionId
                            && root.GetProperty("data").GetProperty("session").GetProperty("status").GetString() == "Active";
                    }).ConfigureAwait(false);
                    AssertEqual(sessionId, createdEvent.GetProperty("data").GetProperty("session").GetProperty("id").GetString());

                    await UpdateCaptainRuntimeAsync(captainId, "Custom").ConfigureAwait(false);

                    HttpResponseMessage sendResponse = await _AuthClient.PostAsync(
                        "/api/v1/objective-refinement-sessions/" + sessionId + "/messages",
                        JsonHelper.ToJsonContent(new
                        {
                            Content = "Clarify acceptance criteria for the backlog item."
                        })).ConfigureAwait(false);
                    sendResponse.EnsureSuccessStatusCode();

                    JsonElement userMessageEvent = await WaitForEventAsync(ws, root =>
                    {
                        return root.GetProperty("type").GetString() == "objective-refinement-session.message.created"
                            && root.GetProperty("data").GetProperty("sessionId").GetString() == sessionId
                            && root.GetProperty("data").GetProperty("message").GetProperty("role").GetString() == "User";
                    }).ConfigureAwait(false);
                    AssertEqual(sessionId, userMessageEvent.GetProperty("data").GetProperty("sessionId").GetString());

                    JsonElement assistantCreatedEvent = await WaitForEventAsync(ws, root =>
                    {
                        return root.GetProperty("type").GetString() == "objective-refinement-session.message.created"
                            && root.GetProperty("data").GetProperty("sessionId").GetString() == sessionId
                            && root.GetProperty("data").GetProperty("message").GetProperty("role").GetString() == "Assistant";
                    }).ConfigureAwait(false);
                    string assistantMessageId = assistantCreatedEvent.GetProperty("data").GetProperty("message").GetProperty("id").GetString()
                        ?? throw new Exception("Assistant refinement message id not found in WebSocket payload");

                    JsonElement assistantUpdatedEvent = await WaitForEventAsync(ws, root =>
                    {
                        return root.GetProperty("type").GetString() == "objective-refinement-session.message.updated"
                            && root.GetProperty("data").GetProperty("sessionId").GetString() == sessionId
                            && root.GetProperty("data").GetProperty("message").GetProperty("id").GetString() == assistantMessageId
                            && !String.IsNullOrWhiteSpace(root.GetProperty("data").GetProperty("message").GetProperty("content").GetString());
                    }).ConfigureAwait(false);
                    AssertContains("Refinement response failed", assistantUpdatedEvent.GetProperty("data").GetProperty("message").GetProperty("content").GetString() ?? String.Empty);

                    JsonElement activeEvent = await WaitForEventAsync(ws, root =>
                    {
                        return root.GetProperty("type").GetString() == "objective-refinement-session.changed"
                            && root.GetProperty("data").GetProperty("session").GetProperty("id").GetString() == sessionId
                            && root.GetProperty("data").GetProperty("session").GetProperty("status").GetString() == "Active";
                    }).ConfigureAwait(false);
                    AssertEqual(sessionId, activeEvent.GetProperty("data").GetProperty("session").GetProperty("id").GetString());

                    HttpResponseMessage deleteSessionResponse = await _AuthClient.DeleteAsync("/api/v1/objective-refinement-sessions/" + sessionId).ConfigureAwait(false);
                    AssertEqual(System.Net.HttpStatusCode.NoContent, deleteSessionResponse.StatusCode);

                    JsonElement deletedEvent = await WaitForEventAsync(ws, root =>
                    {
                        return root.GetProperty("type").GetString() == "objective-refinement-session.deleted"
                            && root.GetProperty("data").GetProperty("sessionId").GetString() == sessionId;
                    }).ConfigureAwait(false);
                    AssertEqual(sessionId, deletedEvent.GetProperty("data").GetProperty("sessionId").GetString());
                    sessionId = String.Empty;
                }
                finally
                {
                    if (!String.IsNullOrWhiteSpace(sessionId))
                        await _AuthClient.DeleteAsync("/api/v1/objective-refinement-sessions/" + sessionId).ConfigureAwait(false);
                    if (!String.IsNullOrWhiteSpace(objectiveId))
                        await _AuthClient.DeleteAsync("/api/v1/backlog/" + objectiveId).ConfigureAwait(false);
                    if (!String.IsNullOrWhiteSpace(captainId))
                        await _AuthClient.DeleteAsync("/api/v1/captains/" + captainId).ConfigureAwait(false);
                }
            }).ConfigureAwait(false);

            // Status Tests
            await RunTest("Status_ReturnsArmadaStatus", async () =>
            {
                JsonElement resp = await WsCommandAsync("status").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("status", resp.GetProperty("action").GetString());
                ArmadaStatus status = DeserializeData<ArmadaStatus>(resp);
                AssertTrue(status.TotalCaptains >= 0);
            }).ConfigureAwait(false);

            await RunTest("StopAll_ReturnsAllStopped", async () =>
            {
                try
                {
                    JsonElement resp = await WsCommandAsync("stop_all").ConfigureAwait(false);
                    AssertEqual("command.result", resp.GetProperty("type").GetString());
                    AssertEqual("stop_all", resp.GetProperty("action").GetString());
                    AssertEqual("all_stopped", resp.GetProperty("data").GetProperty("status").GetString());
                }
                catch (TaskCanceledException)
                {
                    // StopAll may take longer than expected with many active captains - acceptable
                }
                catch (System.Net.WebSockets.WebSocketException)
                {
                    // StopAll may close the WebSocket connection - acceptable
                }
            }).ConfigureAwait(false);

            // Fleet Tests
            await RunTest("ListFleets_Empty_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_fleets").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_fleets", resp.GetProperty("action").GetString());
                EnumerationResult<Fleet> data = DeserializeData<EnumerationResult<Fleet>>(resp);
                AssertNotNull(data.Objects);
            }).ConfigureAwait(false);

            await RunTest("CreateFleet_ReturnsCreatedFleet", async () =>
            {
                JsonElement resp = await WsCommandAsync("create_fleet", new { data = new { Name = "ws-fleet" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("create_fleet", resp.GetProperty("action").GetString());
                Fleet data = DeserializeData<Fleet>(resp);
                AssertStartsWith("flt_", data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetFleet_ExistingFleet_ReturnsFleet", async () =>
            {
                string fleetId = await CreateFleetViaRestAsync("ws-get-fleet").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("get_fleet", new { id = fleetId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                FleetDetailResponse data = DeserializeData<FleetDetailResponse>(resp);
                AssertEqual(fleetId, data.Fleet!.Id);
                AssertNotNull(data.Vessels);
            }).ConfigureAwait(false);

            await RunTest("GetFleet_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_fleet", new { id = "flt_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Fleet not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("UpdateFleet_ExistingFleet_ReturnsUpdated", async () =>
            {
                string fleetId = await CreateFleetViaRestAsync("ws-upd-fleet").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("update_fleet", new { id = fleetId, data = new { Name = "ws-upd-fleet-renamed" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Fleet data = DeserializeData<Fleet>(resp);
                AssertEqual(fleetId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("UpdateFleet_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("update_fleet", new { id = "flt_nonexistent", data = new { Name = "x" } }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Fleet not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("DeleteFleet_ExistingFleet_ReturnsDeleted", async () =>
            {
                string fleetId = await CreateFleetViaRestAsync("ws-del-fleet").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("delete_fleet", new { id = fleetId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                DeleteFleetResponse data = DeserializeData<DeleteFleetResponse>(resp);
                AssertEqual("deleted", data.Status);
            }).ConfigureAwait(false);

            await RunTest("ListFleets_AfterCreate_ReturnsFleet", async () =>
            {
                await WsCommandAsync("create_fleet", new { data = new { Name = "ws-list-fleet" } }).ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("list_fleets").ConfigureAwait(false);
                EnumerationResult<Fleet> data = DeserializeData<EnumerationResult<Fleet>>(resp);
                AssertTrue(data.TotalRecords >= 1);
            }).ConfigureAwait(false);

            await RunTest("ListFleets_WithPagination_RespectsPageSize", async () =>
            {
                await CreateFleetViaRestAsync("ws-page-1").ConfigureAwait(false);
                await CreateFleetViaRestAsync("ws-page-2").ConfigureAwait(false);
                await CreateFleetViaRestAsync("ws-page-3").ConfigureAwait(false);

                JsonElement resp = await WsCommandAsync("list_fleets", new { query = new { pageSize = 2, pageNumber = 1 } }).ConfigureAwait(false);
                EnumerationResult<Fleet> data = DeserializeData<EnumerationResult<Fleet>>(resp);
                AssertEqual(2, data.PageSize);
                AssertEqual(2, data.Objects.Count);
            }).ConfigureAwait(false);

            // Vessel Tests
            await RunTest("ListVessels_Empty_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_vessels").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_vessels", resp.GetProperty("action").GetString());
            }).ConfigureAwait(false);

            await RunTest("CreateVessel_ReturnsCreatedVessel", async () =>
            {
                JsonElement resp = await WsCommandAsync("create_vessel", new { data = new { Name = "ws-vessel", RepoUrl = TestRepoHelper.GetLocalBareRepoUrl() } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Vessel data = DeserializeData<Vessel>(resp);
                AssertStartsWith("vsl_", data.Id);
            }).ConfigureAwait(false);

            await RunTest("CreateVessel_GitHubTokenOverrideDoesNotLeak", async () =>
            {
                string token = "ghp_ws_" + Guid.NewGuid().ToString("N").Substring(0, 10);
                JsonElement resp = await WsCommandAsync("create_vessel", new
                {
                    data = new
                    {
                        Name = "ws-vessel-github-override",
                        RepoUrl = TestRepoHelper.GetLocalBareRepoUrl(),
                        GitHubTokenOverride = token
                    }
                }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                string raw = resp.GetRawText();
                AssertFalse(raw.Contains(token, StringComparison.Ordinal));
                AssertFalse(raw.Contains("\"gitHubTokenOverride\"", StringComparison.Ordinal));
                Vessel data = DeserializeData<Vessel>(resp);
                AssertTrue(data.HasGitHubTokenOverride);
            }).ConfigureAwait(false);

            await RunTest("GetVessel_ExistingVessel_ReturnsVessel", async () =>
            {
                string vesselId = await CreateVesselViaRestAsync("ws-get-vessel").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("get_vessel", new { id = vesselId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Vessel data = DeserializeData<Vessel>(resp);
                AssertEqual(vesselId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetVessel_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_vessel", new { id = "vsl_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Vessel not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("UpdateVessel_ExistingVessel_ReturnsUpdated", async () =>
            {
                string vesselId = await CreateVesselViaRestAsync("ws-upd-vessel").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("update_vessel", new { id = vesselId, data = new { Name = "ws-upd-vessel-renamed", RepoUrl = TestRepoHelper.GetLocalBareRepoUrl() } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Vessel data = DeserializeData<Vessel>(resp);
                AssertEqual(vesselId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("UpdateVessel_EmptyGitHubTokenOverrideClearsOverride", async () =>
            {
                JsonElement createResponse = await WsCommandAsync("create_vessel", new
                {
                    data = new
                    {
                        Name = "ws-clear-github-override",
                        RepoUrl = TestRepoHelper.GetLocalBareRepoUrl(),
                        GitHubTokenOverride = "ghp_ws_clear"
                    }
                }).ConfigureAwait(false);
                Vessel created = DeserializeData<Vessel>(createResponse);
                AssertTrue(created.HasGitHubTokenOverride);

                JsonElement updateResponse = await WsCommandAsync("update_vessel", new
                {
                    id = created.Id,
                    data = new
                    {
                        Name = "ws-clear-github-override",
                        RepoUrl = TestRepoHelper.GetLocalBareRepoUrl(),
                        GitHubTokenOverride = ""
                    }
                }).ConfigureAwait(false);
                Vessel updated = DeserializeData<Vessel>(updateResponse);
                AssertFalse(updated.HasGitHubTokenOverride);
            }).ConfigureAwait(false);

            await RunTest("UpdateVessel_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("update_vessel", new { id = "vsl_nonexistent", data = new { Name = "x", RepoUrl = TestRepoHelper.GetLocalBareRepoUrl() } }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("DeleteVessel_ExistingVessel_ReturnsDeleted", async () =>
            {
                string vesselId = await CreateVesselViaRestAsync("ws-del-vessel").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("delete_vessel", new { id = vesselId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                DeleteVesselResponse data = DeserializeData<DeleteVesselResponse>(resp);
                AssertEqual("deleted", data.Status);
            }).ConfigureAwait(false);

            // Voyage Tests
            await RunTest("ListVoyages_Empty_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_voyages").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_voyages", resp.GetProperty("action").GetString());
            }).ConfigureAwait(false);

            await RunTest("CreateVoyage_BareVoyage_ReturnsCreatedVoyage", async () =>
            {
                JsonElement resp = await WsCommandAsync("create_voyage", new { data = new { title = "ws-voyage", description = "test voyage" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("create_voyage", resp.GetProperty("action").GetString());
                Voyage data = DeserializeData<Voyage>(resp);
                AssertStartsWith("vyg_", data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetVoyage_ExistingVoyage_ReturnsVoyageWithMissions", async () =>
            {
                string voyageId = await CreateVoyageViaRestAsync("ws-get-voyage").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("get_voyage", new { id = voyageId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                VoyageDetailResponse data = DeserializeData<VoyageDetailResponse>(resp);
                AssertNotNull(data.Voyage);
                AssertNotNull(data.Missions);
            }).ConfigureAwait(false);

            await RunTest("GetVoyage_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_voyage", new { id = "vyg_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Voyage not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("CancelVoyage_ExistingVoyage_ReturnsCancelled", async () =>
            {
                string voyageId = await CreateVoyageViaRestAsync("ws-cancel-voyage").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("cancel_voyage", new { id = voyageId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                CancelVoyageResponse data = DeserializeData<CancelVoyageResponse>(resp);
                AssertEqual("Cancelled", data.Voyage!.Status.ToString());
                AssertTrue(data.CancelledMissions >= 0);
            }).ConfigureAwait(false);

            await RunTest("CancelVoyage_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("cancel_voyage", new { id = "vyg_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Voyage not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("PurgeVoyage_ExistingVoyage_ReturnsDeleted", async () =>
            {
                string voyageId = await CreateVoyageViaRestAsync("ws-purge-voyage").ConfigureAwait(false);

                // Cancel first — purge is blocked on Open/InProgress voyages
                await WsCommandAsync("cancel_voyage", new { id = voyageId }).ConfigureAwait(false);

                JsonElement resp = await WsCommandAsync("purge_voyage", new { id = voyageId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                PurgeVoyageResponse data = DeserializeData<PurgeVoyageResponse>(resp);
                AssertEqual("deleted", data.Status);
            }).ConfigureAwait(false);

            await RunTest("PurgeVoyage_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("purge_voyage", new { id = "vyg_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Voyage not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("ListVoyages_AfterCreate_ReturnsVoyage", async () =>
            {
                await WsCommandAsync("create_voyage", new { data = new { title = "ws-list-voyage" } }).ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("list_voyages").ConfigureAwait(false);
                EnumerationResult<Voyage> data = DeserializeData<EnumerationResult<Voyage>>(resp);
                AssertTrue(data.TotalRecords >= 1);
            }).ConfigureAwait(false);

            // Mission Tests
            await RunTest("ListMissions_Empty_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_missions").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_missions", resp.GetProperty("action").GetString());
                EnumerationResult<Mission> data = DeserializeData<EnumerationResult<Mission>>(resp);
                AssertNotNull(data);
            }).ConfigureAwait(false);

            await RunTest("ListMissionSummaries_Empty_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_missions_summary").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_missions_summary", resp.GetProperty("action").GetString());
                EnumerationResult<MissionSummary> data = DeserializeData<EnumerationResult<MissionSummary>>(resp);
                AssertNotNull(data);
            }).ConfigureAwait(false);

            await RunTest("CreateMission_ReturnsCreatedMission", async () =>
            {
                JsonElement resp = await WsCommandAsync("create_mission", new { data = new { Title = "ws-mission" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Mission data = DeserializeData<Mission>(resp);
                AssertStartsWith("msn_", data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetMission_ExistingMission_ReturnsMission", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-get-mission").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("get_mission", new { id = missionId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Mission data = DeserializeData<Mission>(resp);
                AssertEqual(missionId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetMission_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_mission", new { id = "msn_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Mission not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("UpdateMission_ExistingMission_ReturnsUpdated", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-upd-mission").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("update_mission", new { id = missionId, data = new { Title = "ws-upd-mission-renamed" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Mission data = DeserializeData<Mission>(resp);
                AssertEqual(missionId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("UpdateMission_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("update_mission", new { id = "msn_nonexistent", data = new { Title = "x" } }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("CancelMission_ExistingMission_ReturnsCancelled", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-cancel-mission").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("cancel_mission", new { id = missionId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Mission data = DeserializeData<Mission>(resp);
                AssertEqual("Cancelled", data.Status.ToString());
                AssertEqual(missionId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("CancelMission_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("cancel_mission", new { id = "msn_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Mission not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("TransitionMissionStatus_ValidTransition_ReturnsUpdated", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-transition-mission").ConfigureAwait(false);

                // Pending -> Assigned via REST
                await _AuthClient.PutAsync("/api/v1/missions/" + missionId + "/status",
                    JsonHelper.ToJsonContent(new { Status = "Assigned" })).ConfigureAwait(false);

                // Assigned -> InProgress via WebSocket
                JsonElement resp = await WsCommandAsync("transition_mission_status", new { id = missionId, status = "InProgress" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Mission data = DeserializeData<Mission>(resp);
                AssertEqual("InProgress", data.Status.ToString());
            }).ConfigureAwait(false);

            await RunTest("TransitionMissionStatus_InvalidTransition_ReturnsError", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-bad-transition").ConfigureAwait(false);

                // Pending -> Complete is not valid
                JsonElement resp = await WsCommandAsync("transition_mission_status", new { id = missionId, status = "Complete" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertContains("Invalid transition", resp.GetProperty("error").GetString()!);
            }).ConfigureAwait(false);

            await RunTest("TransitionMissionStatus_InvalidStatusString_ReturnsError", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-bad-status").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("transition_mission_status", new { id = missionId, status = "BogusStatus" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertContains("Invalid status", resp.GetProperty("error").GetString()!);
            }).ConfigureAwait(false);

            await RunTest("TransitionMissionStatus_NonExistentMission_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("transition_mission_status", new { id = "msn_nonexistent", status = "InProgress" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Mission not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("TransitionMissionStatus_ToComplete_SetsCompletedUtc", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-complete-mission").ConfigureAwait(false);

                // Pending -> Assigned -> InProgress via REST
                await _AuthClient.PutAsync("/api/v1/missions/" + missionId + "/status",
                    JsonHelper.ToJsonContent(new { Status = "Assigned" })).ConfigureAwait(false);
                await _AuthClient.PutAsync("/api/v1/missions/" + missionId + "/status",
                    JsonHelper.ToJsonContent(new { Status = "InProgress" })).ConfigureAwait(false);

                JsonElement resp = await WsCommandAsync("transition_mission_status", new { id = missionId, status = "Complete" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Mission data = DeserializeData<Mission>(resp);
                AssertNotNull(data.CompletedUtc);
            }).ConfigureAwait(false);

            await RunTest("ListMissions_WithPagination_RespectsPageSize", async () =>
            {
                await CreateMissionViaRestAsync("ws-page-m1").ConfigureAwait(false);
                await CreateMissionViaRestAsync("ws-page-m2").ConfigureAwait(false);
                await CreateMissionViaRestAsync("ws-page-m3").ConfigureAwait(false);

                JsonElement resp = await WsCommandAsync("list_missions", new { query = new { pageSize = 2 } }).ConfigureAwait(false);
                EnumerationResult<Mission> data = DeserializeData<EnumerationResult<Mission>>(resp);
                AssertEqual(2, data.Objects.Count);
            }).ConfigureAwait(false);

            await RunTest("ListMissionSummaries_WithPagination_RespectsPageSize", async () =>
            {
                await CreateMissionViaRestAsync("ws-page-summary-m1").ConfigureAwait(false);
                await CreateMissionViaRestAsync("ws-page-summary-m2").ConfigureAwait(false);
                await CreateMissionViaRestAsync("ws-page-summary-m3").ConfigureAwait(false);

                JsonElement resp = await WsCommandAsync("list_missions_summary", new { query = new { pageSize = 2 } }).ConfigureAwait(false);
                EnumerationResult<MissionSummary> data = DeserializeData<EnumerationResult<MissionSummary>>(resp);
                AssertEqual(2, data.Objects.Count);
            }).ConfigureAwait(false);

            // Captain Tests
            await RunTest("ListCaptains_Empty_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_captains").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_captains", resp.GetProperty("action").GetString());
            }).ConfigureAwait(false);

            await RunTest("CreateCaptain_ReturnsCreatedCaptain", async () =>
            {
                JsonElement resp = await WsCommandAsync("create_captain", new { data = new { Name = "ws-captain", Runtime = "ClaudeCode" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Captain data = DeserializeData<Captain>(resp);
                AssertStartsWith("cpt_", data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetCaptain_ExistingCaptain_ReturnsCaptain", async () =>
            {
                string captainId = await CreateCaptainViaRestAsync("ws-get-captain").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("get_captain", new { id = captainId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Captain data = DeserializeData<Captain>(resp);
                AssertEqual(captainId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetCaptain_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_captain", new { id = "cpt_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Captain not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("UpdateCaptain_ExistingCaptain_ReturnsUpdated", async () =>
            {
                string captainId = await CreateCaptainViaRestAsync("ws-upd-captain").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("update_captain", new { id = captainId, data = new { Name = "ws-upd-captain-renamed", Runtime = "ClaudeCode" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Captain data = DeserializeData<Captain>(resp);
                AssertEqual(captainId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("UpdateCaptain_PreservesOperationalFields", async () =>
            {
                string captainId = await CreateCaptainViaRestAsync("ws-preserve-captain").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("update_captain", new { id = captainId, data = new { Name = "ws-preserve-renamed", Runtime = "ClaudeCode" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                Captain data = DeserializeData<Captain>(resp);
                AssertEqual("Idle", data.State.ToString());
            }).ConfigureAwait(false);

            await RunTest("UpdateCaptain_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("update_captain", new { id = "cpt_nonexistent", data = new { Name = "x" } }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Captain not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("DeleteCaptain_ExistingCaptain_ReturnsDeleted", async () =>
            {
                string captainId = await CreateCaptainViaRestAsync("ws-del-captain").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("delete_captain", new { id = captainId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                DeleteCaptainResponse data = DeserializeData<DeleteCaptainResponse>(resp);
                AssertEqual("deleted", data.Status);
            }).ConfigureAwait(false);

            await RunTest("DeleteCaptain_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("delete_captain", new { id = "cpt_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Captain not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("StopCaptain_ExistingCaptain_ReturnsStopped", async () =>
            {
                string captainId = await CreateCaptainViaRestAsync("ws-stop-captain").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("stop_captain", new { captainId = captainId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                StopCaptainResponse data = DeserializeData<StopCaptainResponse>(resp);
                AssertEqual("stopped", data.Status);
            }).ConfigureAwait(false);

            // Signal Tests
            await RunTest("ListSignals_Empty_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_signals").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_signals", resp.GetProperty("action").GetString());
            }).ConfigureAwait(false);

            await RunTest("SendSignal_ReturnsCreatedSignal", async () =>
            {
                JsonElement resp = await WsCommandAsync("send_signal", new { data = new { Type = "Nudge", Payload = "hello" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("send_signal", resp.GetProperty("action").GetString());
                Signal data = DeserializeData<Signal>(resp);
                AssertStartsWith("sig_", data.Id);
            }).ConfigureAwait(false);

            await RunTest("ListSignals_AfterSend_ReturnsSignal", async () =>
            {
                await WsCommandAsync("send_signal", new { data = new { Type = "Mail", Payload = "test-mail" } }).ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("list_signals").ConfigureAwait(false);
                EnumerationResult<Signal> data = DeserializeData<EnumerationResult<Signal>>(resp);
                AssertTrue(data.TotalRecords >= 1);
            }).ConfigureAwait(false);

            // Event Tests
            await RunTest("ListEvents_ReturnsEventList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_events").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_events", resp.GetProperty("action").GetString());
                EnumerationResult<ArmadaEvent> data = DeserializeData<EnumerationResult<ArmadaEvent>>(resp);
                AssertNotNull(data.Objects);
            }).ConfigureAwait(false);

            // Dock Tests
            await RunTest("ListDocks_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_docks").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_docks", resp.GetProperty("action").GetString());
                EnumerationResult<Dock> data = DeserializeData<EnumerationResult<Dock>>(resp);
                AssertNotNull(data.Objects);
            }).ConfigureAwait(false);

            // MergeQueue Tests
            await RunTest("ListMergeQueue_ReturnsEmptyList", async () =>
            {
                JsonElement resp = await WsCommandAsync("list_merge_queue").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("list_merge_queue", resp.GetProperty("action").GetString());
            }).ConfigureAwait(false);

            await RunTest("EnqueueMerge_ReturnsCreatedEntry", async () =>
            {
                JsonElement resp = await WsCommandAsync("enqueue_merge", new { data = new { BranchName = "feature/ws-test", TargetBranch = "main" } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("enqueue_merge", resp.GetProperty("action").GetString());
                MergeEntry data = DeserializeData<MergeEntry>(resp);
                AssertStartsWith("mrg_", data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetMergeEntry_ExistingEntry_ReturnsEntry", async () =>
            {
                JsonElement createResp = await WsCommandAsync("enqueue_merge", new { data = new { BranchName = "feature/ws-get-merge", TargetBranch = "main" } }).ConfigureAwait(false);
                MergeEntry created = DeserializeData<MergeEntry>(createResp);
                string mergeId = created.Id;

                JsonElement resp = await WsCommandAsync("get_merge_entry", new { id = mergeId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                MergeEntry data = DeserializeData<MergeEntry>(resp);
                AssertEqual(mergeId, data.Id);
            }).ConfigureAwait(false);

            await RunTest("GetMergeEntry_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_merge_entry", new { id = "mrg_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertEqual("Merge entry not found", resp.GetProperty("error").GetString());
            }).ConfigureAwait(false);

            await RunTest("CancelMerge_ExistingEntry_ReturnsCancelled", async () =>
            {
                JsonElement createResp = await WsCommandAsync("enqueue_merge", new { data = new { BranchName = "feature/ws-cancel-merge", TargetBranch = "main" } }).ConfigureAwait(false);
                MergeEntry created = DeserializeData<MergeEntry>(createResp);
                string mergeId = created.Id;

                JsonElement resp = await WsCommandAsync("cancel_merge", new { id = mergeId }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                CancelMergeResponse data = DeserializeData<CancelMergeResponse>(resp);
                AssertEqual("cancelled", data.Status);
            }).ConfigureAwait(false);

            await RunTest("ProcessMergeQueue_ReturnsProcessed", async () =>
            {
                JsonElement resp = await WsCommandAsync("process_merge_queue").ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                ProcessMergeQueueResponse data = DeserializeData<ProcessMergeQueueResponse>(resp);
                AssertEqual("processed", data.Status);
            }).ConfigureAwait(false);

            // ── Mission Diff/Log and Captain Log ────────────────────────

            await RunTest("GetMissionDiff_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_mission_diff", new { id = "msn_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("GetMissionDiff_ExistingMission_ReturnsResult", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-diff-mission").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("get_mission_diff", new { id = missionId }).ConfigureAwait(false);
                // May return error (no worktree/settings) or result — just verify action is correct
                string action = resp.GetProperty("action").GetString()!;
                AssertEqual("get_mission_diff", action);
            }).ConfigureAwait(false);

            await RunTest("GetMissionLog_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_mission_log", new { id = "msn_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("GetMissionLog_ExistingMission_ReturnsLogData", async () =>
            {
                string missionId = await CreateMissionViaRestAsync("ws-log-mission").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("get_mission_log", new { id = missionId }).ConfigureAwait(false);
                // May return error (settings not configured) or result with empty log
                string action = resp.GetProperty("action").GetString()!;
                AssertEqual("get_mission_log", action);
            }).ConfigureAwait(false);

            await RunTest("GetCaptainLog_NonExistent_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("get_captain_log", new { id = "cpt_nonexistent" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("GetCaptainLog_ExistingCaptain_ReturnsLogData", async () =>
            {
                string captainId = await CreateCaptainViaRestAsync("ws-log-captain").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("get_captain_log", new { id = captainId }).ConfigureAwait(false);
                // May return error (settings not configured) or result with empty log
                string action = resp.GetProperty("action").GetString()!;
                AssertEqual("get_captain_log", action);
            }).ConfigureAwait(false);

            // Note: stop_server is not tested in automated suite as it would shut down the server.

            // ── Enumerate ─────────────────────────────────────────────

            await RunTest("Enumerate_Fleets_ReturnsPaginatedResult", async () =>
            {
                await CreateFleetViaRestAsync("ws-enum-fleet").ConfigureAwait(false);
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "fleets", query = new { pageSize = 10, pageNumber = 1 } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
                AssertEqual("enumerate", resp.GetProperty("action").GetString());
                EnumerationResult<Fleet> data = DeserializeData<EnumerationResult<Fleet>>(resp);
                AssertNotNull(data.Objects);
            }).ConfigureAwait(false);

            await RunTest("Enumerate_Vessels_ReturnsResult", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "vessels" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_Captains_ReturnsResult", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "captains" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_Missions_ReturnsResult", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "missions" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_Voyages_ReturnsResult", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "voyages" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_Docks_ReturnsResult", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "docks" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_Signals_ReturnsResult", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "signals" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_Events_ReturnsResult", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "events" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_MergeQueue_ReturnsResult", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "merge_queue" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_WithPagination_RespectsPageSize", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "fleets", query = new { pageSize = 5, pageNumber = 1 } }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_SingularEntityType_Works", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "fleet" }).ConfigureAwait(false);
                AssertEqual("command.result", resp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("Enumerate_InvalidEntityType_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("enumerate", new { entityType = "bananas" }).ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertContains("Unknown entity type", resp.GetProperty("error").GetString()!);
            }).ConfigureAwait(false);

            // Error Handling Tests
            await RunTest("UnknownAction_ReturnsError", async () =>
            {
                JsonElement resp = await WsCommandAsync("totally_bogus_action").ConfigureAwait(false);
                AssertEqual("command.error", resp.GetProperty("type").GetString());
                AssertContains("Unknown action", resp.GetProperty("error").GetString()!);
            }).ConfigureAwait(false);

            await RunTest("UnknownRoute_ReturnsError", async () =>
            {
                using ClientWebSocket ws = await ConnectAsync().ConfigureAwait(false);

                string msg = JsonHelper.Serialize(new { Route = "bad_route" });
                byte[] bytes = Encoding.UTF8.GetBytes(msg);
                await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);

                byte[] buffer = new byte[1048576];
                using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
                WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token).ConfigureAwait(false);
                string json = Encoding.UTF8.GetString(buffer, 0, result.Count);
                using JsonDocument doc = JsonDocument.Parse(json);
                JsonElement root = doc.RootElement;

                AssertEqual("error", root.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("NoRoute_ReturnsError", async () =>
            {
                using ClientWebSocket ws = await ConnectAsync().ConfigureAwait(false);

                string msg = JsonHelper.Serialize(new { hello = "world" });
                byte[] bytes = Encoding.UTF8.GetBytes(msg);
                await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);

                byte[] buffer = new byte[1048576];
                using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
                WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token).ConfigureAwait(false);
                string json = Encoding.UTF8.GetString(buffer, 0, result.Count);
                using JsonDocument doc = JsonDocument.Parse(json);
                JsonElement root = doc.RootElement;

                AssertEqual("error", root.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            // CrossEntity Tests
            await RunTest("FullFleetLifecycle_CreateGetUpdateDelete", async () =>
            {
                // Create
                JsonElement createResp = await WsCommandAsync("create_fleet", new { data = new { Name = "lifecycle-fleet" } }).ConfigureAwait(false);
                AssertEqual("command.result", createResp.GetProperty("type").GetString());
                Fleet createdFleet = DeserializeData<Fleet>(createResp);
                string fleetId = createdFleet.Id;
                AssertStartsWith("flt_", fleetId);

                // Get
                JsonElement getResp = await WsCommandAsync("get_fleet", new { id = fleetId }).ConfigureAwait(false);
                AssertEqual("command.result", getResp.GetProperty("type").GetString());
                FleetDetailResponse getDetail = DeserializeData<FleetDetailResponse>(getResp);
                AssertEqual(fleetId, getDetail.Fleet!.Id);

                // Update
                JsonElement updateResp = await WsCommandAsync("update_fleet", new { id = fleetId, data = new { Name = "lifecycle-fleet-updated" } }).ConfigureAwait(false);
                AssertEqual("command.result", updateResp.GetProperty("type").GetString());

                // Delete
                JsonElement deleteResp = await WsCommandAsync("delete_fleet", new { id = fleetId }).ConfigureAwait(false);
                AssertEqual("command.result", deleteResp.GetProperty("type").GetString());
                DeleteFleetResponse deleted = DeserializeData<DeleteFleetResponse>(deleteResp);
                AssertEqual("deleted", deleted.Status);

                // Verify deleted
                JsonElement verifyResp = await WsCommandAsync("get_fleet", new { id = fleetId }).ConfigureAwait(false);
                AssertEqual("command.error", verifyResp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("FullVoyageLifecycle_CreateGetCancelPurge", async () =>
            {
                // Create bare voyage
                JsonElement createResp = await WsCommandAsync("create_voyage", new { data = new { title = "lifecycle-voyage", description = "test" } }).ConfigureAwait(false);
                AssertEqual("command.result", createResp.GetProperty("type").GetString());
                Voyage createdVoyage = DeserializeData<Voyage>(createResp);
                string voyageId = createdVoyage.Id;

                // Get
                JsonElement getResp = await WsCommandAsync("get_voyage", new { id = voyageId }).ConfigureAwait(false);
                AssertEqual("command.result", getResp.GetProperty("type").GetString());

                // Create another voyage for purge test
                JsonElement create2Resp = await WsCommandAsync("create_voyage", new { data = new { title = "purge-voyage" } }).ConfigureAwait(false);
                Voyage createdVoyage2 = DeserializeData<Voyage>(create2Resp);
                string purgeId = createdVoyage2.Id;

                // Cancel first
                JsonElement cancelResp = await WsCommandAsync("cancel_voyage", new { id = voyageId }).ConfigureAwait(false);
                AssertEqual("command.result", cancelResp.GetProperty("type").GetString());
                CancelVoyageResponse cancelData = DeserializeData<CancelVoyageResponse>(cancelResp);
                AssertEqual("Cancelled", cancelData.Voyage!.Status.ToString());

                // Cancel second before purge — purge is blocked on Open/InProgress voyages
                await WsCommandAsync("cancel_voyage", new { id = purgeId }).ConfigureAwait(false);

                // Purge second
                JsonElement purgeResp = await WsCommandAsync("purge_voyage", new { id = purgeId }).ConfigureAwait(false);
                AssertEqual("command.result", purgeResp.GetProperty("type").GetString());
                PurgeVoyageResponse purgeData = DeserializeData<PurgeVoyageResponse>(purgeResp);
                AssertEqual("deleted", purgeData.Status);

                // Verify purged
                JsonElement verifyResp = await WsCommandAsync("get_voyage", new { id = purgeId }).ConfigureAwait(false);
                AssertEqual("command.error", verifyResp.GetProperty("type").GetString());
            }).ConfigureAwait(false);

            await RunTest("FullMissionLifecycle_CreateTransitionCancel", async () =>
            {
                // Create
                JsonElement createResp = await WsCommandAsync("create_mission", new { data = new { Title = "lifecycle-mission" } }).ConfigureAwait(false);
                AssertEqual("command.result", createResp.GetProperty("type").GetString());
                Mission createdMission = DeserializeData<Mission>(createResp);
                string missionId = createdMission.Id;

                // Transition: Pending -> Assigned
                JsonElement t1 = await WsCommandAsync("transition_mission_status", new { id = missionId, status = "Assigned" }).ConfigureAwait(false);
                AssertEqual("command.result", t1.GetProperty("type").GetString());

                // Transition: Assigned -> InProgress
                JsonElement t2 = await WsCommandAsync("transition_mission_status", new { id = missionId, status = "InProgress" }).ConfigureAwait(false);
                AssertEqual("command.result", t2.GetProperty("type").GetString());

                // Cancel
                JsonElement cancelResp = await WsCommandAsync("cancel_mission", new { id = missionId }).ConfigureAwait(false);
                AssertEqual("command.result", cancelResp.GetProperty("type").GetString());
                Mission cancelledMission = DeserializeData<Mission>(cancelResp);
                AssertEqual("Cancelled", cancelledMission.Status.ToString());
            }).ConfigureAwait(false);

            await RunTest("FullCaptainLifecycle_CreateUpdateDelete", async () =>
            {
                // Create
                JsonElement createResp = await WsCommandAsync("create_captain", new { data = new { Name = "lifecycle-captain", Runtime = "ClaudeCode" } }).ConfigureAwait(false);
                AssertEqual("command.result", createResp.GetProperty("type").GetString());
                Captain createdCaptain = DeserializeData<Captain>(createResp);
                string captainId = createdCaptain.Id;

                // Get
                JsonElement getResp = await WsCommandAsync("get_captain", new { id = captainId }).ConfigureAwait(false);
                AssertEqual("command.result", getResp.GetProperty("type").GetString());

                // Update
                JsonElement updateResp = await WsCommandAsync("update_captain", new { id = captainId, data = new { Name = "lifecycle-renamed", Runtime = "ClaudeCode" } }).ConfigureAwait(false);
                AssertEqual("command.result", updateResp.GetProperty("type").GetString());

                // Delete
                JsonElement deleteResp = await WsCommandAsync("delete_captain", new { id = captainId }).ConfigureAwait(false);
                AssertEqual("command.result", deleteResp.GetProperty("type").GetString());

                // Verify deleted
                JsonElement verifyResp = await WsCommandAsync("get_captain", new { id = captainId }).ConfigureAwait(false);
                AssertEqual("command.error", verifyResp.GetProperty("type").GetString());
            }).ConfigureAwait(false);
        }

        #endregion

        #region Private-Methods

        private async Task<ClientWebSocket> ConnectAsync()
        {
            ClientWebSocket ws = new ClientWebSocket();
            Uri uri = new Uri("ws://127.0.0.1:" + _RestPort + "/ws");
            await ws.ConnectAsync(uri, CancellationToken.None).ConfigureAwait(false);
            return ws;
        }

        private async Task SubscribeAsync(ClientWebSocket ws)
        {
            string payload = JsonHelper.Serialize(new { Route = "subscribe" });
            byte[] bytes = Encoding.UTF8.GetBytes(payload);
            await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);
            await WaitForEventAsync(ws, root => root.GetProperty("type").GetString() == "status.snapshot").ConfigureAwait(false);
        }

        private async Task<JsonElement> WaitForEventAsync(ClientWebSocket ws, Func<JsonElement, bool> predicate, int timeoutMs = 15000)
        {
            byte[] buffer = new byte[1048576];
            using CancellationTokenSource cts = new CancellationTokenSource(timeoutMs);

            while (true)
            {
                WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token).ConfigureAwait(false);
                string json = Encoding.UTF8.GetString(buffer, 0, result.Count);
                using JsonDocument doc = JsonDocument.Parse(json);
                JsonElement root = doc.RootElement.Clone();

                if (predicate(root))
                    return root;
            }
        }

        /// <summary>
        /// Extract the "data" property from a WsCommandAsync JsonElement result and deserialize it to T.
        /// </summary>
        private T DeserializeData<T>(JsonElement resp)
        {
            string dataJson = resp.GetProperty("data").GetRawText();
            return JsonHelper.Deserialize<T>(dataJson);
        }

        private async Task<JsonElement> WsCommandAsync(string action, object? extraFields = null)
        {
            using ClientWebSocket ws = await ConnectAsync().ConfigureAwait(false);

            Dictionary<string, object?> msg = new Dictionary<string, object?>
            {
                ["Route"] = "command",
                ["action"] = action
            };

            if (extraFields != null)
            {
                string extraJson = JsonHelper.Serialize(extraFields);
                using JsonDocument extraDoc = JsonDocument.Parse(extraJson);
                foreach (JsonProperty prop in extraDoc.RootElement.EnumerateObject())
                {
                    msg[prop.Name] = JsonSerializer.Deserialize<object>(prop.Value.GetRawText());
                }
            }

            string payload = JsonHelper.Serialize(msg);
            byte[] bytes = Encoding.UTF8.GetBytes(payload);
            await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);

            byte[] buffer = new byte[1048576];
            using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

            // Loop past broadcast messages (mission.changed, voyage.changed, etc.)
            // until we receive the actual command.result or command.error response.
            while (true)
            {
                WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token).ConfigureAwait(false);
                string json = Encoding.UTF8.GetString(buffer, 0, result.Count);
                using JsonDocument doc = JsonDocument.Parse(json);
                JsonElement root = doc.RootElement.Clone();

                if (root.TryGetProperty("type", out JsonElement typeElem))
                {
                    string? type = typeElem.GetString();
                    if (type == "command.result" || type == "command.error")
                        return root;
                }

                // Not a command response — skip and read next message
            }
        }

        private async Task<string> CreateFleetViaRestAsync(string name)
        {
            HttpResponseMessage resp = await _AuthClient.PostAsync("/api/v1/fleets",
                JsonHelper.ToJsonContent(new { Name = name })).ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();
            Fleet fleet = await JsonHelper.DeserializeAsync<Fleet>(resp).ConfigureAwait(false);
            return fleet.Id;
        }

        private async Task<string> CreateVesselViaRestAsync(string name)
        {
            HttpResponseMessage resp = await _AuthClient.PostAsync("/api/v1/vessels",
                JsonHelper.ToJsonContent(new { Name = name, RepoUrl = TestRepoHelper.GetLocalBareRepoUrl() })).ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();
            Vessel vessel = await JsonHelper.DeserializeAsync<Vessel>(resp).ConfigureAwait(false);
            return vessel.Id;
        }

        private async Task<string> CreateCaptainViaRestAsync(string name)
        {
            HttpResponseMessage resp = await _AuthClient.PostAsync("/api/v1/captains",
                JsonHelper.ToJsonContent(new { Name = name, Runtime = "ClaudeCode" })).ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();
            Captain captain = await JsonHelper.DeserializeAsync<Captain>(resp).ConfigureAwait(false);
            return captain.Id;
        }

        private async Task<Captain> CreateCaptainRecordViaRestAsync(string name)
        {
            HttpResponseMessage resp = await _AuthClient.PostAsync("/api/v1/captains",
                JsonHelper.ToJsonContent(new { Name = name, Runtime = "ClaudeCode" })).ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();
            return await JsonHelper.DeserializeAsync<Captain>(resp).ConfigureAwait(false);
        }

        private async Task<Objective> CreateObjectiveViaRestAsync(string title)
        {
            HttpResponseMessage resp = await _AuthClient.PostAsync("/api/v1/backlog",
                JsonHelper.ToJsonContent(new { Title = title, Description = "WebSocket objective coverage" })).ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();
            return await JsonHelper.DeserializeAsync<Objective>(resp).ConfigureAwait(false);
        }

        private async Task UpdateCaptainRuntimeAsync(string captainId, string runtime)
        {
            HttpResponseMessage getResp = await _AuthClient.GetAsync("/api/v1/captains/" + captainId).ConfigureAwait(false);
            getResp.EnsureSuccessStatusCode();
            Captain captain = await JsonHelper.DeserializeAsync<Captain>(getResp).ConfigureAwait(false);

            HttpResponseMessage updateResp = await _AuthClient.PutAsync(
                "/api/v1/captains/" + captainId,
                JsonHelper.ToJsonContent(new
                {
                    Name = captain.Name,
                    Runtime = runtime
                })).ConfigureAwait(false);
            updateResp.EnsureSuccessStatusCode();
        }

        private async Task<string> CreateMissionViaRestAsync(string title)
        {
            HttpResponseMessage resp = await _AuthClient.PostAsync("/api/v1/missions",
                JsonHelper.ToJsonContent(new { Title = title })).ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();

            // When mission stays Pending (no captain available), the API returns
            // { "Mission": {...}, "Warning": "..." } instead of the mission directly.
            string body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
            MissionCreateResponse wrapper = JsonHelper.Deserialize<MissionCreateResponse>(body);
            if (wrapper.Mission != null)
                return wrapper.Mission.Id;

            // Fallback: response is the mission directly
            Mission mission = JsonHelper.Deserialize<Mission>(body);
            return mission.Id;
        }

        private async Task<string> CreateVoyageViaRestAsync(string title)
        {
            HttpResponseMessage resp = await _AuthClient.PostAsync("/api/v1/voyages",
                JsonHelper.ToJsonContent(new { Title = title })).ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();
            Voyage voyage = await JsonHelper.DeserializeAsync<Voyage>(resp).ConfigureAwait(false);
            return voyage.Id;
        }

        #endregion
    }
}
