/**
 * L14 — memory feature wired to the stateful `MockMemoryGateway` via the real
 * `DIProvider` (same harness as `skills.test.tsx`).
 *
 * Covers:
 * - index rendering (title + hook + type badge) on mount
 * - create a note (New → fill name/description/type/content → Save) → appears in
 *   the list, and `createMemory` receives the right payload
 * - edit a note (slug read-only; description/type/content editable) → persisted
 * - delete a note → removed from the list
 * - `[[slug]]` link resolution (existing target listed; broken link ignored)
 * - basic error state (gateway rejects → alert message shown)
 *
 * Also includes `MockMemoryGateway` unit tests (CRUD + index derivation +
 * `resolveLinks` ignoring broken links).
 */

import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";

import {
  MockMemoryGateway,
  MockEmbedderGateway,
  MockSystemGateway,
} from "@/adapters/mock";
import type { GatewayError } from "@/domain";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { MemoryPanel } from "./MemoryPanel";

const PROJECT_ID = "proj-memory-test";

function renderMemoryPanel(
  memory?: MockMemoryGateway,
  system: MockSystemGateway = new MockSystemGateway(),
) {
  const mem = memory ?? new MockMemoryGateway();
  const gateways = {
    memory: mem,
    embedder: new MockEmbedderGateway(),
    system,
  } as unknown as Gateways;
  return {
    memory: mem,
    system,
    ...render(
      <DIProvider gateways={gateways}>
        <MemoryPanel projectId={PROJECT_ID} />
      </DIProvider>,
    ),
  };
}

async function waitForMemoryIdle() {
  await waitFor(() => {
    expect(
      (
        screen.getByRole("button", { name: "create memory" }) as HTMLButtonElement
      ).disabled,
    ).toBe(false);
  });
}

/** Opens the editor in create-mode, fills it, and saves. */
async function createMemory(
  name: string,
  description: string,
  content: string,
  type: "user" | "feedback" | "project" | "reference" = "project",
) {
  await waitForMemoryIdle();
  fireEvent.click(screen.getByRole("button", { name: "create memory" }));
  await waitFor(() => expect(screen.getByLabelText("memory name")).toBeTruthy());
  fireEvent.change(screen.getByLabelText("memory name"), {
    target: { value: name },
  });
  fireEvent.change(screen.getByLabelText("memory description"), {
    target: { value: description },
  });
  fireEvent.change(screen.getByLabelText("memory type"), {
    target: { value: type },
  });
  fireEvent.change(screen.getByLabelText("memory content"), {
    target: { value: content },
  });
  fireEvent.click(screen.getByRole("button", { name: "Save note" }));
}

// ---------------------------------------------------------------------------
// MemoryPanel feature tests
// ---------------------------------------------------------------------------

describe("MemoryPanel (with MockMemoryGateway)", () => {
  it("shows the empty state initially", async () => {
    renderMemoryPanel();
    await waitForMemoryIdle();
    expect(screen.getByText("No memory notes yet.")).toBeTruthy();
  });

  it("renders the index (title + hook + type) on mount", async () => {
    const memory = new MockMemoryGateway();
    await memory.createMemory({
      projectId: PROJECT_ID,
      name: "Build AppImage",
      description: "use APPIMAGE_EXTRACT_AND_RUN=1",
      type: "reference",
      content: "# notes",
    });
    renderMemoryPanel(memory);

    await screen.findByText("Build AppImage");
    expect(screen.getByText("use APPIMAGE_EXTRACT_AND_RUN=1")).toBeTruthy();
    // Type badge.
    expect(screen.getByText("reference")).toBeTruthy();
  });

  it("creating a note adds it to the list with the right payload", async () => {
    const { memory } = renderMemoryPanel();
    const spy = vi.spyOn(memory, "createMemory");

    await createMemory("My Note", "a one-line hook", "# body", "user");
    await screen.findByText("My Note");

    // Payload received by the gateway.
    expect(spy).toHaveBeenCalledWith({
      projectId: PROJECT_ID,
      name: "My Note",
      description: "a one-line hook",
      type: "user",
      content: "# body",
    });

    // Persisted in the gateway under the derived slug.
    const note = await memory.getMemory(PROJECT_ID, "my-note");
    expect(note.name).toBe("My Note");
    expect(note.description).toBe("a one-line hook");
    expect(note.type).toBe("user");
    expect(note.content).toBe("# body");
  });

  it("Save is disabled until description and content are filled", async () => {
    renderMemoryPanel();
    await waitForMemoryIdle();
    fireEvent.click(screen.getByRole("button", { name: "create memory" }));
    await waitFor(() =>
      expect(screen.getByRole("button", { name: "Save note" })).toBeTruthy(),
    );
    const save = screen.getByRole("button", {
      name: "Save note",
    }) as HTMLButtonElement;
    expect(save.disabled).toBe(true);

    // Filling everything enables Save.
    fireEvent.change(screen.getByLabelText("memory name"), {
      target: { value: "X" },
    });
    fireEvent.change(screen.getByLabelText("memory description"), {
      target: { value: "h" },
    });
    fireEvent.change(screen.getByLabelText("memory content"), {
      target: { value: "c" },
    });
    expect(
      (screen.getByRole("button", { name: "Save note" }) as HTMLButtonElement)
        .disabled,
    ).toBe(false);
  });

  it("editing a note keeps the slug read-only and persists the changes", async () => {
    const { memory } = renderMemoryPanel();
    await createMemory("Editable Note", "old hook", "old body", "project");
    await screen.findByText("Editable Note");

    fireEvent.click(
      screen.getByRole("button", { name: "edit memory Editable Note" }),
    );

    // Slug shown read-only — there's no editable "memory name" input in edit-mode.
    await waitFor(() => expect(screen.getByText("editable-note")).toBeTruthy());
    expect(screen.queryByLabelText("memory name")).toBeNull();

    // Description / type / content are editable.
    fireEvent.change(screen.getByLabelText("memory description"), {
      target: { value: "new hook" },
    });
    fireEvent.change(screen.getByLabelText("memory type"), {
      target: { value: "feedback" },
    });
    fireEvent.change(screen.getByLabelText("memory content"), {
      target: { value: "new body" },
    });
    fireEvent.click(screen.getByRole("button", { name: "Save note" }));

    await waitFor(async () => {
      const note = await memory.getMemory(PROJECT_ID, "editable-note");
      expect(note.description).toBe("new hook");
      expect(note.type).toBe("feedback");
      expect(note.content).toBe("new body");
    });
  });

  it("deleting a note removes it from the list", async () => {
    renderMemoryPanel();
    await createMemory("To Delete", "hook", "body", "project");
    await screen.findByText("To Delete");

    fireEvent.click(
      screen.getByRole("button", { name: "delete memory To Delete" }),
    );
    await waitFor(() => expect(screen.queryByText("To Delete")).toBeNull());
  });

  it("resolves [[slug]] links: existing target listed, broken link ignored", async () => {
    const memory = new MockMemoryGateway();
    // Target note that the linker will reference.
    await memory.createMemory({
      projectId: PROJECT_ID,
      name: "Target Note",
      description: "the target",
      type: "project",
      content: "# target",
    });
    // Source note links to the existing target and to a non-existent slug.
    await memory.createMemory({
      projectId: PROJECT_ID,
      name: "Source Note",
      description: "the source",
      type: "project",
      content: "See [[target-note]] and [[ghost-slug]].",
    });
    renderMemoryPanel(memory);

    fireEvent.click(
      await screen.findByRole("button", { name: "edit memory Source Note" }),
    );

    // The resolved link to the existing note appears; the broken one does not.
    await waitFor(() => expect(screen.getByText("target-note")).toBeTruthy());
    expect(screen.queryByText("ghost-slug")).toBeNull();
  });

  it("surfaces an error message when the gateway rejects", async () => {
    const memory = new MockMemoryGateway();
    const err: GatewayError = { code: "INTERNAL", message: "boom from gateway" };
    vi.spyOn(memory, "readIndex").mockRejectedValue(err);

    renderMemoryPanel(memory);

    const alert = await screen.findByRole("alert");
    expect(alert.textContent).toMatch(/boom from gateway/i);
  });

  it("refreshes the index when a memorySaved domain event fires", async () => {
    const memory = new MockMemoryGateway();
    const system = new MockSystemGateway();
    const spy = vi.spyOn(memory, "readIndex");
    renderMemoryPanel(memory, system);
    await waitForMemoryIdle();
    spy.mockClear();

    await memory.createMemory({
      projectId: PROJECT_ID,
      name: "Harvested Note",
      description: "created by idea-memory",
      type: "project",
      content: "harvested body",
    });
    system.emit({ type: "memorySaved", slug: "harvested-note" });

    expect(await screen.findByText("Harvested Note")).toBeTruthy();
    expect(spy).toHaveBeenCalled();
  });

  it("does not refresh the index for unrelated domain events", async () => {
    const memory = new MockMemoryGateway();
    const system = new MockSystemGateway();
    const spy = vi.spyOn(memory, "readIndex");
    renderMemoryPanel(memory, system);
    await waitForMemoryIdle();
    spy.mockClear();

    system.emit({ type: "projectCreated", projectId: "other-project" });

    await waitFor(() => expect(spy).not.toHaveBeenCalled());
  });
});

// ---------------------------------------------------------------------------
// MockMemoryGateway unit tests
// ---------------------------------------------------------------------------

describe("MockMemoryGateway (unit)", () => {
  it("CRUD: create → get → update → delete by immutable slug", async () => {
    const gw = new MockMemoryGateway();
    await gw.createMemory({
      projectId: "p",
      name: "Hello World",
      description: "d",
      type: "project",
      content: "c",
    });

    const got = await gw.getMemory("p", "hello-world");
    expect(got.name).toBe("Hello World");

    await gw.updateMemory("p", "hello-world", "d2", "user", "c2");
    const updated = await gw.getMemory("p", "hello-world");
    expect(updated.description).toBe("d2");
    expect(updated.type).toBe("user");
    expect(updated.content).toBe("c2");
    // Name (and therefore slug) is immutable.
    expect(updated.name).toBe("Hello World");

    await gw.deleteMemory("p", "hello-world");
    await expect(gw.getMemory("p", "hello-world")).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("getMemory / updateMemory / deleteMemory throw NOT_FOUND for unknown slug", async () => {
    const gw = new MockMemoryGateway();
    await expect(gw.getMemory("p", "ghost")).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
    await expect(
      gw.updateMemory("p", "ghost", "d", "project", "c"),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
    await expect(gw.deleteMemory("p", "ghost")).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("readIndex derives the recall index from the notes", async () => {
    const gw = new MockMemoryGateway();
    await gw.createMemory({
      projectId: "p",
      name: "Indexed Note",
      description: "hooky",
      type: "reference",
      content: "x",
    });
    const index = await gw.readIndex("p");
    expect(index).toEqual([
      { slug: "indexed-note", title: "Indexed Note", hook: "hooky", type: "reference" },
    ]);
  });

  it("notes are partitioned by project", async () => {
    const gw = new MockMemoryGateway();
    await gw.createMemory({
      projectId: "p1",
      name: "A",
      description: "d",
      type: "project",
      content: "c",
    });
    expect(await gw.readIndex("p1")).toHaveLength(1);
    expect(await gw.readIndex("p2")).toHaveLength(0);
  });

  it("resolveLinks keeps existing targets and ignores broken / self links", async () => {
    const gw = new MockMemoryGateway();
    await gw.createMemory({
      projectId: "p",
      name: "Alpha",
      description: "d",
      type: "project",
      content: "c",
    });
    await gw.createMemory({
      projectId: "p",
      name: "Beta",
      description: "d",
      type: "project",
      content: "Links [[alpha]], [[ghost]] and self [[beta]].",
    });
    const links = await gw.resolveLinks("p", "beta");
    expect(links).toEqual(["alpha"]);
  });
});
