import test from "node:test";
import assert from "node:assert/strict";
import { initialConversationState, reduceConversationState, sessionOwnerId } from "./conversation-state.js";

test("conversation stays loading until the initial session snapshot arrives", () => {
  const loading = reduceConversationState(initialConversationState, {
    type: "session.replaced",
    sessionId: "session-1",
    events: []
  });
  assert.equal(loading.loading, true);

  const loaded = reduceConversationState(loading, {
    type: "snapshot.received",
    events: []
  });
  assert.equal(loaded.loading, false);
});

test("an authoritative conversation replacement skips the snapshot loading state", () => {
  const loaded = reduceConversationState(initialConversationState, {
    type: "session.replaced",
    sessionId: "session-1",
    events: [],
    loaded: true
  });

  assert.equal(loaded.loading, false);
});

test("session replacement installs history and clears draft", () => {
  const events = [{ id: "history" }];
  const state = reduceConversationState(
    { ...initialConversationState, message: "old" },
    { type: "session.replaced", sessionId: "s", events }
  );
  assert.equal(state.sessionId, "s");
  assert.deepEqual(state.events, events);
  assert.equal(state.message, "");
});

test("session replacement restores the persisted draft", () => {
  const state = reduceConversationState(
    initialConversationState,
    { type: "session.replaced", sessionId: "s", events: [], message: "continue steering" }
  );

  assert.equal(state.message, "continue steering");
});

test("stream event appends through event coalescing", () => {
  const state = reduceConversationState(initialConversationState, {
    type: "event.received",
    entry: { id: "1", event: "message.accepted", data: { text: "hello" } }
  });
  assert.equal(state.events.length, 1);
});

test("a stream event burst appends in one reducer action", () => {
  const state = reduceConversationState(
    { ...initialConversationState, sessionId: "session-1" },
    {
      type: "events.received",
      sessionId: "session-1",
      entries: [
        { id: "1", event: "message.accepted", data: { text: "hello" } },
        { id: "2", event: "session.output", data: { kind: "assistant_delta", text: "a" } },
        { id: "3", event: "session.output", data: { kind: "assistant_delta", text: "b" } }
      ]
    }
  );

  assert.equal(state.events.length, 2);
  assert.equal(state.events.at(-1).data.text, "ab");
});

test("a stale stream event burst cannot enter another session", () => {
  const current = { ...initialConversationState, sessionId: "session-2" };
  const state = reduceConversationState(current, {
    type: "events.received",
    sessionId: "session-1",
    entries: [{ id: "old", event: "message.accepted", data: { text: "stale" } }]
  });

  assert.equal(state, current);
});

test("submitted messages appear immediately and clear the draft", () => {
  const state = reduceConversationState(
    { ...initialConversationState, message: "Start now" },
    { type: "message.submitted", id: "optimistic-1", text: "Start now", createdAt: "2026-06-30T00:00:00.000Z" }
  );

  assert.equal(state.message, "");
  assert.deepEqual(state.events, [{
    id: "optimistic-1",
    event: "message.accepted",
    data: { text: "Start now", optimistic: true },
    createdAt: "2026-06-30T00:00:00.000Z"
  }]);
});

test("queued optimistic submissions keep a visible queued marker", () => {
  const state = reduceConversationState(
    { ...initialConversationState, message: "Next" },
    { type: "message.submitted", id: "optimistic-queued", text: "Next", queued: true, createdAt: "2026-06-30T00:00:00.000Z" }
  );

  assert.deepEqual(state.events, [{
    id: "optimistic-queued",
    event: "message.accepted",
    data: { text: "Next", queued: true, optimistic: true },
    createdAt: "2026-06-30T00:00:00.000Z"
  }]);
});

test("submitted attachments appear immediately with a fallback label", () => {
  const state = reduceConversationState(
    { ...initialConversationState, message: "" },
    {
      type: "message.submitted",
      id: "optimistic-attachment",
      text: "",
      attachmentCount: 2,
      attachments: [
        { id: "att-1", name: "a.jpg", previewUrl: "blob:one" },
        { id: "att-2", name: "b.jpg", previewUrl: "blob:two" }
      ],
      createdAt: "2026-06-30T00:00:00.000Z"
    }
  );

  assert.equal(state.message, "");
  assert.deepEqual(state.events, [{
    id: "optimistic-attachment",
    event: "message.accepted",
    data: {
      text: "",
      attachmentCount: 2,
      attachments: [
        { id: "att-1", name: "a.jpg", previewUrl: "blob:one" },
        { id: "att-2", name: "b.jpg", previewUrl: "blob:two" }
      ],
      optimistic: true
    },
    createdAt: "2026-06-30T00:00:00.000Z"
  }]);
});

test("server acceptance preserves optimistic browser attachment previews", () => {
  const state = reduceConversationState(
    {
      ...initialConversationState,
      events: [{
        id: "optimistic-attachment",
        event: "message.accepted",
        data: {
          text: "",
          attachmentCount: 1,
          attachments: [{ id: "att-1", name: "photo.jpg", previewUrl: "blob:photo" }],
          optimistic: true
        },
        createdAt: "2026-06-30T00:00:00.000Z"
      }]
    },
    {
      type: "event.received",
      entry: {
        id: "server-attachment",
        event: "message.accepted",
        data: { text: "", attachmentCount: 1, sessionId: "session-1" },
        createdAt: "2026-06-30T00:00:01.000Z"
      }
    }
  );

  assert.deepEqual(state.events, [{
    id: "optimistic-attachment",
    event: "message.accepted",
    data: {
      text: "",
      attachmentCount: 1,
      sessionId: "session-1",
      optimisticAccepted: true,
      acceptedAt: "2026-06-30T00:00:01.000Z",
      attachments: [{ id: "att-1", name: "photo.jpg", previewUrl: "blob:photo" }]
    },
    createdAt: "2026-06-30T00:00:00.000Z"
  }]);
});

test("server message acceptance replaces matching optimistic message", () => {
  const state = reduceConversationState(
    {
      ...initialConversationState,
      events: [{
        id: "optimistic-1",
        event: "message.accepted",
        data: { text: "Start now", optimistic: true },
        createdAt: "2026-06-30T00:00:00.000Z"
      }]
    },
    {
      type: "event.received",
      entry: {
        id: "server-1",
        event: "message.accepted",
        data: { text: "Start now", sessionId: "session-1" },
        createdAt: "2026-06-30T00:00:01.000Z"
      }
    }
  );

  assert.deepEqual(state.events, [{
    id: "optimistic-1",
    event: "message.accepted",
    data: {
      text: "Start now",
      sessionId: "session-1",
      optimisticAccepted: true,
      acceptedAt: "2026-06-30T00:00:01.000Z"
    },
    createdAt: "2026-06-30T00:00:00.000Z"
  }]);
});

test("failed submitted messages are removed and restored to the draft", () => {
  const state = reduceConversationState(
    {
      ...initialConversationState,
      events: [{
        id: "optimistic-1",
        event: "message.accepted",
        data: { text: "Start now", optimistic: true },
        createdAt: "2026-06-30T00:00:00.000Z"
      }]
    },
    { type: "message.failed", id: "optimistic-1", text: "Start now" }
  );

  assert.equal(state.message, "Start now");
  assert.deepEqual(state.events, []);
});

test("backend queue disposition removes a stale optimistic chat bubble", () => {
  const state = reduceConversationState(
    {
      ...initialConversationState,
      events: [{ id: "optimistic-1", event: "message.accepted", data: { text: "Later", optimistic: true } }]
    },
    { type: "message.queued", id: "optimistic-1" }
  );

  assert.deepEqual(state.events, []);
});

test("backend queue disposition keeps a just-submitted optimistic chat bubble when requested", () => {
  const state = reduceConversationState(
    {
      ...initialConversationState,
      events: [{ id: "optimistic-1", event: "message.accepted", data: { text: "Stop soon", optimistic: true } }]
    },
    { type: "message.queued", id: "optimistic-1", keepOptimistic: true }
  );

  assert.deepEqual(state.events, [
    { id: "optimistic-1", event: "message.accepted", data: { text: "Stop soon", optimistic: true } }
  ]);
});

test("stop requested immediately marks the latest optimistic turn interrupted", () => {
  const state = reduceConversationState(
    {
      ...initialConversationState,
      events: [{ id: "optimistic-1", event: "message.accepted", data: { text: "Stop soon", optimistic: true }, createdAt: "2026-06-30T00:00:00.000Z" }]
    },
    { type: "stop.requested", createdAt: "2026-06-30T00:00:01.000Z" }
  );

  assert.deepEqual(state.events.map((event) => event.event), ["message.accepted", "session.status"]);
  assert.deepEqual(state.events[1].data, { status: "interrupted", optimistic: true });
});

test("failed stop removes the optimistic interrupted status", () => {
  const state = reduceConversationState(
    {
      ...initialConversationState,
      events: [
        { id: "optimistic-1", event: "message.accepted", data: { text: "Stop soon", optimistic: true } },
        { id: "optimistic-stop", event: "session.status", data: { status: "interrupted", optimistic: true } }
      ]
    },
    { type: "stop.failed", id: "optimistic-stop" }
  );

  assert.deepEqual(state.events.map((event) => event.id), ["optimistic-1"]);
});

test("snapshot preserves pending optimistic message and stop state", () => {
  const state = reduceConversationState(
    {
      ...initialConversationState,
      events: [
        {
          id: "optimistic-1",
          event: "message.accepted",
          data: { text: "Stop soon", optimistic: true },
          createdAt: "2026-06-30T00:00:00.000Z"
        },
        {
          id: "optimistic-stop",
          event: "session.status",
          data: { status: "interrupted", optimistic: true },
          createdAt: "2026-06-30T00:00:01.000Z"
        }
      ]
    },
    { type: "snapshot.received", events: [] }
  );

  assert.deepEqual(state.events.map((event) => event.id), ["optimistic-1", "optimistic-stop"]);
});

test("approval decisions optimistically resolve and can be restored on failure", () => {
  const current = {
    ...initialConversationState,
    events: [
      { id: "message-1", event: "message.accepted", data: { text: "Run it" }, createdAt: "2026-06-30T00:00:00.000Z" },
      {
        id: "approval-1",
        event: "approval.requested",
        data: { id: "approval-1", status: "pending", title: "Command approval required" },
        createdAt: "2026-06-30T00:00:01.000Z"
      }
    ]
  };

  const decided = reduceConversationState(current, {
    type: "approval.decision.started",
    id: "approval-1",
    decision: "approved",
    createdAt: "2026-06-30T00:00:02.000Z"
  });
  const restored = reduceConversationState(decided, {
    type: "approval.decision.failed",
    id: "approval-1",
    createdAt: "2026-06-30T00:00:03.000Z"
  });

  assert.equal(decided.events.at(-1).event, "approval.resolved");
  assert.equal(decided.events.at(-1).data.status, "approved");
  assert.equal(restored.events.at(-1).event, "approval.requested");
  assert.equal(restored.events.at(-1).data.status, "pending");
});

test("session owner overrides selected account for thread actions", () => {
  assert.equal(sessionOwnerId({ accountId: "desktop" }, "a"), "desktop");
  assert.equal(sessionOwnerId(null, "a"), "a");
});
