/**
 * L4 — the write-portal hook {@link useWritePortal} (ARCHITECTURE §20).
 *
 * Driven with fake timers + a fake handle recording writes, and the real
 * {@link DIProvider} wiring the in-memory {@link MockSystemGateway} (to emit
 * `delegationReady`) and {@link MockInputGateway} (to record the ack).
 *
 * Cases (cadrage §20.6):
 *  - injects NOTHING while the human line is non-empty (counter > 0);
 *  - at an empty line → writes `text` WITHOUT `\n`, then `\r` after the delay;
 *  - K=2 race → exactly two `\x7f` before the text;
 *  - 2 s floor → overlay/suspension held until 2000 ms after step (b);
 *  - the relay is suspended during the overlay;
 *  - `delegationDelivered` is acked exactly once after (d);
 *  - default submitSequence/delay applied when the profile omits them.
 */
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
import { act, renderHook } from "@testing-library/react";

import type { Gateways, TerminalHandle } from "@/ports";
import {
  MockInputGateway,
  MockSystemGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { useWritePortal } from "./useWritePortal";

function makeHandle(): { handle: TerminalHandle; writes: string[] } {
  const writes: string[] = [];
  const handle: TerminalHandle = {
    sessionId: "s1",
    write: vi.fn(async (b: Uint8Array) => {
      writes.push(new TextDecoder().decode(b));
    }),
    resize: vi.fn().mockResolvedValue(undefined),
    detach: vi.fn(),
    close: vi.fn().mockResolvedValue(undefined),
  };
  return { handle, writes };
}

function setup(agentId: string | null = "ag1") {
  const system = new MockSystemGateway();
  const input = new MockInputGateway();
  const gateways = { system, input } as unknown as Gateways;
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <DIProvider gateways={gateways}>{children}</DIProvider>
  );
  const view = renderHook(() => useWritePortal("p1", agentId), { wrapper });
  return { system, input, view };
}

const PROFILELESS = (ticket: string, text: string) => ({
  type: "delegationReady" as const,
  agentId: "ag1",
  ticket,
  text,
});

beforeEach(() => {
  vi.useFakeTimers();
});
afterEach(() => {
  vi.useRealTimers();
});

describe("useWritePortal (§20)", () => {
  it("injects NOTHING while the human line is non-empty, releases at empty line", async () => {
    const { system, view } = setup();
    const { handle, writes } = makeHandle();

    // Bind a live handle and type a printable char (line non-empty).
    act(() => view.result.current.portal.bindHandle(handle));
    act(() => view.result.current.portal.onHumanData("a")); // K = 1

    // A delegation arrives while the line is non-empty.
    await act(async () => {
      system.emit(PROFILELESS("t1", "hello"));
    });
    // Nothing written: the delegation waits.
    expect(writes.length).toBe(0);

    // The human presses Enter → line empty → injection fires.
    await act(async () => {
      view.result.current.portal.onHumanData("\r");
      await vi.runAllTimersAsync();
    });

    // text written without trailing newline, then the submit sequence.
    expect(writes).toContain("hello");
    expect(writes[writes.length - 1]).toBe("\r");
    expect(writes.join("")).not.toContain("\n");
  });

  it("at an empty line writes text then \\r after the delay, acking once", async () => {
    const { system, input, view } = setup();
    const { handle, writes } = makeHandle();
    act(() => view.result.current.portal.bindHandle(handle));

    await act(async () => {
      system.emit(PROFILELESS("t1", "do it"));
      // Flush the microtask that defers step (c)/(d) but NOT the submit delay.
      await Promise.resolve();
      await Promise.resolve();
    });
    // Line is empty (counter 0) ⇒ handshake started; overlay raised.
    expect(view.result.current.overlay).toBe(true);

    // Before the delay elapses, only the text has been written (no submit yet).
    expect(writes).toEqual(["do it"]);

    await act(async () => {
      await vi.runAllTimersAsync();
    });

    expect(writes).toEqual(["do it", "\r"]);
    // Acked exactly once with the right shape.
    expect(input.delivered).toEqual([
      { projectId: "p1", agentId: "ag1", ticket: "t1" },
    ]);
  });

  it("chunks long delegated text before submit and acks after the submit", async () => {
    const { system, input, view } = setup();
    const { handle, writes } = makeHandle();
    act(() => view.result.current.portal.bindHandle(handle));
    const text = `début ${"x".repeat(1200)} fin`;

    await act(async () => {
      system.emit(PROFILELESS("t1", text));
      await vi.runAllTimersAsync();
    });

    expect(writes.length).toBeGreaterThan(2);
    expect(writes[writes.length - 1]).toBe("\r");
    expect(writes.slice(0, -1).join("")).toBe(text);
    expect(input.delivered).toEqual([
      { projectId: "p1", agentId: "ag1", ticket: "t1" },
    ]);
  });

  it("K=2 race → exactly two \\x7f backspaces before the text (never Ctrl-U)", async () => {
    const { system, view } = setup();
    const { handle, writes } = makeHandle();
    act(() => view.result.current.portal.bindHandle(handle));

    // The delegation arrives at an EMPTY line, so the handshake starts and
    // raises suspension. The §20.6 race: in the micro-window between "empty
    // observed" (b) and step (c) re-checking the counter, the human types K=2
    // printables. TerminalView still reports them while suspended, so the
    // counter is 2 when step (c) reads it ⇒ exactly two backspaces.
    await act(async () => {
      system.emit(PROFILELESS("t1", "TASK"));
      // Race two keystrokes in before the timers (delay) flush step (c)/(d).
      view.result.current.portal.onHumanData("x");
      view.result.current.portal.onHumanData("y");
      await vi.runAllTimersAsync();
    });

    const idxText = writes.indexOf("TASK");
    expect(idxText).toBeGreaterThan(0);
    expect(writes[idxText - 1]).toBe("\x7f\x7f");
    expect(writes.join("")).not.toContain("\x15"); // never Ctrl-U
  });

  it("holds the overlay until the 2 s floor after step (b)", async () => {
    const { system, view } = setup();
    const { handle } = makeHandle();
    act(() => view.result.current.portal.bindHandle(handle));

    await act(async () => {
      system.emit(PROFILELESS("t1", "hi"));
    });
    expect(view.result.current.overlay).toBe(true);

    // Finish the write phase (default 60 ms delay) — still well under 2 s.
    await act(async () => {
      await vi.advanceTimersByTimeAsync(100);
    });
    // The relay is still suspended and the overlay still up (2 s floor).
    expect(view.result.current.portal.isSuspended()).toBe(true);
    expect(view.result.current.overlay).toBe(true);

    // Cross the 2 s floor.
    await act(async () => {
      await vi.advanceTimersByTimeAsync(2000);
    });
    expect(view.result.current.overlay).toBe(false);
    expect(view.result.current.portal.isSuspended()).toBe(false);
  });

  it("suspends the keystroke relay during the overlay", async () => {
    const { system, view } = setup();
    const { handle } = makeHandle();
    act(() => view.result.current.portal.bindHandle(handle));

    expect(view.result.current.portal.isSuspended()).toBe(false);
    await act(async () => {
      system.emit(PROFILELESS("t1", "hi"));
    });
    // During injection the relay is suspended.
    expect(view.result.current.portal.isSuspended()).toBe(true);

    await act(async () => {
      await vi.runAllTimersAsync();
    });
    expect(view.result.current.portal.isSuspended()).toBe(false);
  });

  it("applies the profile submitSequence/submitDelayMs when provided", async () => {
    const { system, view } = setup();
    const { handle, writes } = makeHandle();
    act(() => view.result.current.portal.bindHandle(handle));

    await act(async () => {
      system.emit({
        type: "delegationReady",
        agentId: "ag1",
        ticket: "t1",
        text: "msg",
        submitSequence: "\n",
        submitDelayMs: 200,
      });
    });

    // After 100 ms (< 200) the submit sequence has not been written yet.
    await act(async () => {
      await vi.advanceTimersByTimeAsync(100);
    });
    expect(writes).toEqual(["msg"]);

    await act(async () => {
      await vi.runAllTimersAsync();
    });
    expect(writes).toEqual(["msg", "\n"]);
  });

  it("ignores delegationReady for a different agent", async () => {
    const { system, view } = setup();
    const { handle, writes } = makeHandle();
    act(() => view.result.current.portal.bindHandle(handle));

    await act(async () => {
      system.emit({
        type: "delegationReady",
        agentId: "OTHER",
        ticket: "t1",
        text: "nope",
      });
      await vi.runAllTimersAsync();
    });
    expect(writes.length).toBe(0);
    expect(view.result.current.overlay).toBe(false);
  });

  it("waits for a live handle before injecting", async () => {
    const { system, input, view } = setup();
    const { handle, writes } = makeHandle();

    // No handle yet: an arriving delegation must NOT write or ack.
    await act(async () => {
      system.emit(PROFILELESS("t1", "later"));
      await vi.runAllTimersAsync();
    });
    expect(writes.length).toBe(0);
    expect(input.delivered.length).toBe(0);

    // Handle becomes live (PTY opened) → injection proceeds.
    await act(async () => {
      view.result.current.portal.bindHandle(handle);
      await vi.runAllTimersAsync();
    });
    expect(writes).toEqual(["later", "\r"]);
    expect(input.delivered.length).toBe(1);
  });

  it("reports front attachment only after the delegation subscription is ready", async () => {
    const { input, view } = setup();
    const { handle } = makeHandle();

    act(() => view.result.current.portal.bindHandle(handle));
    expect(input.frontAttached).toEqual([]);

    await act(async () => {
      await Promise.resolve();
    });

    expect(input.frontAttached).toEqual([{ agentId: "ag1", attached: true }]);

    act(() => view.result.current.portal.unbindHandle());
    expect(input.frontAttached).toEqual([
      { agentId: "ag1", attached: true },
      { agentId: "ag1", attached: false },
    ]);
  });

  it("is inert for a plain (agent-less) cell", async () => {
    const { system, view } = setup(null);
    const { handle, writes } = makeHandle();
    act(() => view.result.current.portal.bindHandle(handle));

    await act(async () => {
      system.emit(PROFILELESS("t1", "x"));
      await vi.runAllTimersAsync();
    });
    expect(writes.length).toBe(0);
    expect(view.result.current.overlay).toBe(false);
  });
});
