/**
 * #4 — LayoutTabs / useLayouts: named-layout tab bar driven through the
 * MockLayoutGateway. Covers: list, create, rename, delete, switch.
 */
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor, fireEvent, act } from "@testing-library/react";

import type { Gateways } from "@/ports";
import {
  MockLayoutGateway,
  MockAgentGateway,
  MockTerminalGateway,
  MockSystemGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { LayoutTabs } from "./LayoutTabs";

function renderTabs(
  layout: MockLayoutGateway,
  projectId = "p1",
  onActiveLayoutChange = vi.fn(),
  system?: MockSystemGateway,
) {
  const gateways = {
    layout,
    terminal: new MockTerminalGateway(),
    agent: new MockAgentGateway(),
    ...(system ? { system } : {}),
  } as unknown as Gateways;
  return render(
    <DIProvider gateways={gateways}>
      <LayoutTabs projectId={projectId} onActiveLayoutChange={onActiveLayoutChange} />
    </DIProvider>,
  );
}

describe("LayoutTabs", () => {
  it("renders the default 'Default' tab on first load", async () => {
    const layout = new MockLayoutGateway();
    renderTabs(layout);
    await waitFor(() => {
      expect(screen.getByText("Default")).toBeTruthy();
    });
  });

  it("shows a create (+) button", async () => {
    const layout = new MockLayoutGateway();
    renderTabs(layout);
    await waitFor(() => {
      expect(screen.getByLabelText("create layout")).toBeTruthy();
    });
  });

  it("switching tabs reports the new active layout (id + kind) to the parent", async () => {
    const layout = new MockLayoutGateway();
    // Seed a second layout.
    await layout.createLayout("p1", "Beta");
    const onActiveLayoutChange = vi.fn();
    renderTabs(layout, "p1", onActiveLayoutChange);

    await waitFor(() => {
      expect(screen.getByText("Beta")).toBeTruthy();
    });

    // Click the Beta tab.
    fireEvent.click(screen.getByText("Beta"));

    await waitFor(() => {
      // Called with the full LayoutInfo (so the parent knows the kind too).
      expect(onActiveLayoutChange).toHaveBeenCalledWith(
        expect.objectContaining({ name: "Beta", kind: "terminal" }),
      );
    });
  });

  it("renaming a tab (double-click → type → blur) updates the name", async () => {
    const layout = new MockLayoutGateway();
    renderTabs(layout);

    await waitFor(() => {
      expect(screen.getByText("Default")).toBeTruthy();
    });

    // Double-click to start rename.
    fireEvent.dblClick(screen.getByText("Default"));

    await waitFor(() => {
      expect(screen.getByLabelText(/rename layout/i)).toBeTruthy();
    });

    const input = screen.getByLabelText(/rename layout/i) as HTMLInputElement;
    fireEvent.change(input, { target: { value: "My Layout" } });
    fireEvent.blur(input);

    await waitFor(() => {
      expect(screen.getByText("My Layout")).toBeTruthy();
    });
  });

  it("delete button is disabled when only one layout exists", async () => {
    const layout = new MockLayoutGateway();
    renderTabs(layout);

    await waitFor(() => {
      expect(screen.getByLabelText(/delete layout/i)).toBeTruthy();
    });

    const closeBtn = screen.getByLabelText(/delete layout/i) as HTMLButtonElement;
    expect(closeBtn.disabled).toBe(true);
  });

  it("creating a layout adds a tab", async () => {
    const layout = new MockLayoutGateway();
    // Directly create a second layout via the gateway (simulates the + flow).
    await layout.createLayout("p1", "Extra");
    renderTabs(layout);

    await waitFor(() => {
      expect(screen.getByText("Default")).toBeTruthy();
      expect(screen.getByText("Extra")).toBeTruthy();
    });
  });

  it("deleting a layout removes its tab (when 2 exist)", async () => {
    const layout = new MockLayoutGateway();
    await layout.createLayout("p1", "Second");
    renderTabs(layout);

    await waitFor(() => {
      expect(screen.getByText("Second")).toBeTruthy();
    });

    const deleteBtns = screen.getAllByLabelText(/delete layout/i);
    // Both tabs exist, pick the second (Second).
    await act(async () => {
      fireEvent.click(deleteBtns[1]);
    });

    await waitFor(() => {
      expect(screen.queryByText("Second")).toBeNull();
    });
  });

  // ── Active-layout self-heal (feature/layouts-active-self-heal) ──────────────

  it("switching tabs adopts the backend's authoritative active id", async () => {
    const layout = new MockLayoutGateway();
    const { layoutId: betaId } = await layout.createLayout("p1", "Beta");
    const onActiveLayoutChange = vi.fn();
    renderTabs(layout, "p1", onActiveLayoutChange);

    await waitFor(() => expect(screen.getByText("Beta")).toBeTruthy());

    await act(async () => {
      fireEvent.click(screen.getByText("Beta"));
    });

    await waitFor(() => {
      expect(onActiveLayoutChange).toHaveBeenCalledWith(
        expect.objectContaining({ id: betaId, name: "Beta" }),
      );
    });
  });

  it("on layoutChanged it re-lists and purges a vanished active id (self-heal)", async () => {
    const layout = new MockLayoutGateway();
    const system = new MockSystemGateway();
    const { layoutId: betaId } = await layout.createLayout("p1", "Beta");
    const onActiveLayoutChange = vi.fn();
    renderTabs(layout, "p1", onActiveLayoutChange, system);

    await waitFor(() => expect(screen.getByText("Beta")).toBeTruthy());

    // Make Beta the active layout from the UI.
    await act(async () => {
      fireEvent.click(screen.getByText("Beta"));
    });
    await waitFor(() =>
      expect(onActiveLayoutChange).toHaveBeenCalledWith(
        expect.objectContaining({ id: betaId }),
      ),
    );

    // Simulate an EXTERNAL overwrite of layouts.json: Beta is removed and the
    // backend active id falls back to Default — done straight on the gateway,
    // bypassing the hook, so the hook still locally remembers the (now stale) id.
    const { layouts } = await layout.listLayouts("p1");
    const defaultId = layouts.find((l) => l.name === "Default")!.id;
    await layout.deleteLayout("p1", betaId);

    onActiveLayoutChange.mockClear();
    // The backend notifies the change → the hook re-lists and purges the stale id.
    await act(async () => {
      system.emit({ type: "layoutChanged", projectId: "p1" });
    });

    await waitFor(() => {
      expect(screen.queryByText("Beta")).toBeNull();
      expect(onActiveLayoutChange).toHaveBeenCalledWith(
        expect.objectContaining({ id: defaultId, name: "Default" }),
      );
    });
  });

  it("ignores layoutChanged events for another project", async () => {
    const layout = new MockLayoutGateway();
    const system = new MockSystemGateway();
    await layout.createLayout("p1", "Beta");
    renderTabs(layout, "p1", vi.fn(), system);

    await waitFor(() => expect(screen.getByText("Beta")).toBeTruthy());

    // An event for a different project must not trigger a re-list/purge here.
    await act(async () => {
      system.emit({ type: "layoutChanged", projectId: "other-project" });
    });

    // Both tabs are still present (no spurious reconciliation).
    expect(screen.getByText("Beta")).toBeTruthy();
    expect(screen.getByText("Default")).toBeTruthy();
  });
});
