/**
 * L7 — templates feature + drift/sync, wired to the stateful
 * `MockTemplateGateway` (sharing a `MockAgentGateway`) via the real `DIProvider`.
 *
 * Covers:
 * - createTemplate → template appears in list
 * - updateTemplate → version increments
 * - deleteTemplate → template removed from list
 * - createAgentFromTemplate → agent appears in agent list
 * - drift: after createAgentFromTemplate(synchronized:true) + updateTemplate →
 *   detectDrift returns the agent → badge "update available" shown in AgentsPanel
 * - Sync: clicking the Sync button calls syncAgent → badge disappears
 *
 * Also includes MockTemplateGateway unit tests.
 */

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

import { MockAgentGateway, MockProfileGateway, MockTemplateGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { TemplatesPanel } from "./TemplatesPanel";
import { AgentsPanel } from "@/features/agents/AgentsPanel";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const PROJECT_ID = "proj-tmpl-test";

interface RenderOpts {
  agent?: MockAgentGateway;
  template?: MockTemplateGateway;
  profile?: MockProfileGateway;
}

/**
 * Renders `TemplatesPanel` behind a `DIProvider` with isolated mock gateways.
 * Returns references to the gateway instances for direct inspection.
 */
function renderTemplatesPanel(opts: RenderOpts = {}) {
  const agent = opts.agent ?? new MockAgentGateway();
  const tmpl = opts.template ?? new MockTemplateGateway(agent);
  const profile = opts.profile ?? new MockProfileGateway();
  const gateways = { agent, profile, template: tmpl } as unknown as Gateways;
  return {
    agent,
    tmpl,
    profile,
    ...render(
      <DIProvider gateways={gateways}>
        <TemplatesPanel projectId={PROJECT_ID} />
      </DIProvider>,
    ),
  };
}

/**
 * Renders `AgentsPanel` with the given shared gateways.
 */
function renderAgentsPanel(agent: MockAgentGateway, tmpl: MockTemplateGateway) {
  const profile = new MockProfileGateway();
  const gateways = { agent, profile, template: tmpl } as unknown as Gateways;
  return render(
    <DIProvider gateways={gateways}>
      <AgentsPanel projectId={PROJECT_ID} projectRoot="/tmp/proj" />
    </DIProvider>,
  );
}

/**
 * Waits for the TemplatesPanel to be idle (the "New template" / "create template"
 * button is accessible, meaning the panel has rendered).
 */
async function waitForTemplatesIdle() {
  await waitFor(() => {
    expect(screen.getByRole("button", { name: "create template" })).toBeTruthy();
  });
}

/** Waits for agents panel to be idle (agent name field accessible). */
async function waitForAgentsIdle() {
  await waitFor(() => {
    expect(screen.getByLabelText("agent name")).toBeTruthy();
  });
}

/**
 * Opens the TemplateEditor overlay, fills the create-template form, and saves it.
 * Adapted for the new fullscreen-editor flow:
 *  1. Click the "create template" ("New template") button to open the editor.
 *  2. Fill `template name`, `template content`, optionally `template default profile`.
 *  3. Click "Save template" to submit.
 */
async function createTemplate(
  name: string,
  content = "# Content",
  profileId = "",
) {
  await waitForTemplatesIdle();

  // Open the fullscreen editor overlay
  fireEvent.click(screen.getByRole("button", { name: "create template" }));

  // Wait for the editor to appear
  await waitFor(() => {
    expect(screen.getByLabelText("template name")).toBeTruthy();
  });

  fireEvent.change(screen.getByLabelText("template name"), {
    target: { value: name },
  });
  fireEvent.change(screen.getByLabelText("template content"), {
    target: { value: content },
  });
  if (profileId) {
    fireEvent.change(screen.getByLabelText("template default profile"), {
      target: { value: profileId },
    });
  }

  // Save via the "Save template" button (aria-label on the submit button)
  fireEvent.click(screen.getByRole("button", { name: "Save template" }));
}

// ---------------------------------------------------------------------------
// TemplatesPanel feature tests
// ---------------------------------------------------------------------------

describe("TemplatesPanel (with MockTemplateGateway)", () => {
  it("shows 'No templates yet.' when there are no templates", async () => {
    renderTemplatesPanel();
    await waitForTemplatesIdle();
    expect(screen.getByText("No templates yet.")).toBeTruthy();
  });

  it("creating a template adds it to the list", async () => {
    renderTemplatesPanel();
    await createTemplate("My Template", "## Hello");
    const item = await screen.findByText("My Template");
    expect(item).toBeTruthy();
    // Version 1 shown
    expect(screen.getByText("v1")).toBeTruthy();
  });

  it("the Create button is disabled when the name is empty", async () => {
    renderTemplatesPanel();
    await waitForTemplatesIdle();
    // "New template" button should be enabled (opens the editor)
    const btn = screen.getByRole("button", { name: "create template" });
    // The "New template" button is always enabled — it opens the editor.
    // The Save button *inside* the editor is disabled when name is empty.
    fireEvent.click(btn);
    await waitFor(() => {
      expect(screen.getByRole("button", { name: "Save template" })).toBeTruthy();
    });
    const saveBtn = screen.getByRole("button", { name: "Save template" });
    expect((saveBtn as HTMLButtonElement).disabled).toBe(true);
  });

  it("updating a template increments its version", async () => {
    const agent = new MockAgentGateway();
    const tmpl = new MockTemplateGateway(agent);
    renderTemplatesPanel({ agent, template: tmpl });

    await createTemplate("Versioned");
    await screen.findByText("v1");

    // Click Edit — opens the fullscreen editor for this template
    fireEvent.click(screen.getByRole("button", { name: "edit Versioned" }));

    // Wait for the editor to open
    await waitFor(() => {
      expect(screen.getByLabelText("template content")).toBeTruthy();
    });

    // Edit the content and save
    fireEvent.change(screen.getByLabelText("template content"), {
      target: { value: "# Updated content" },
    });
    fireEvent.click(screen.getByRole("button", { name: "Save template" }));

    // Version should now be 2
    await waitFor(() => {
      expect(screen.getByText("v2")).toBeTruthy();
    });

    // Gateway reflects the update
    const templates = await tmpl.listTemplates();
    expect(templates[0].version).toBe(2);
    expect(templates[0].contentMd).toBe("# Updated content");
  });

  it("deleting a template removes it from the list", async () => {
    renderTemplatesPanel();
    await createTemplate("ToDelete");
    await screen.findByText("ToDelete");

    fireEvent.click(screen.getByRole("button", { name: "delete template ToDelete" }));

    await waitFor(() => {
      expect(screen.queryByText("ToDelete")).toBeNull();
    });
    expect(screen.getByText("No templates yet.")).toBeTruthy();
  });

  it("'Create agent from template' creates an agent in the shared agent gateway", async () => {
    const agent = new MockAgentGateway();
    const tmpl = new MockTemplateGateway(agent);
    renderTemplatesPanel({ agent, template: tmpl });

    await createTemplate("Agent Factory", "## ctx", "p1");
    await screen.findByText("Agent Factory");

    fireEvent.click(
      screen.getByRole("button", { name: "create agent from Agent Factory" }),
    );

    // Verify the agent appears in the shared gateway
    await waitFor(async () => {
      const agents = await agent.listAgents(PROJECT_ID);
      expect(agents).toHaveLength(1);
      expect(agents[0].origin.type).toBe("fromTemplate");
      expect(agents[0].synchronized).toBe(true);
    });
  });

  it("TemplateEditor: can switch Edit/Preview tabs and Save persists the template", async () => {
    const agent = new MockAgentGateway();
    const tmpl = new MockTemplateGateway(agent);
    renderTemplatesPanel({ agent, template: tmpl });

    await waitForTemplatesIdle();

    // Open the editor overlay
    fireEvent.click(screen.getByRole("button", { name: "create template" }));
    await waitFor(() => expect(screen.getByLabelText("template name")).toBeTruthy());

    // Fill in name and content in Edit tab
    fireEvent.change(screen.getByLabelText("template name"), {
      target: { value: "Preview Test" },
    });
    fireEvent.change(screen.getByLabelText("template content"), {
      target: { value: "## Hello Preview" },
    });

    // Switch to Preview tab
    fireEvent.click(screen.getByRole("tab", { name: "Preview" }));
    // The rendered markdown content should be visible (no textarea)
    await waitFor(() => {
      expect(screen.queryByLabelText("template content")).toBeNull();
    });

    // Switch back to Edit tab
    fireEvent.click(screen.getByRole("tab", { name: "Edit" }));
    await waitFor(() => {
      expect(screen.getByLabelText("template content")).toBeTruthy();
    });

    // Save
    fireEvent.click(screen.getByRole("button", { name: "Save template" }));

    // Template should appear in the list
    await screen.findByText("Preview Test");
    const templates = await tmpl.listTemplates();
    expect(templates).toHaveLength(1);
    expect(templates[0].name).toBe("Preview Test");
    expect(templates[0].contentMd).toBe("## Hello Preview");
  });
});

// ---------------------------------------------------------------------------
// Drift + Sync integration tests (AgentsPanel)
// ---------------------------------------------------------------------------

describe("Drift badge and Sync (AgentsPanel + MockTemplateGateway)", () => {
  it("shows 'update available' badge after template is updated and agent has drift", async () => {
    const agent = new MockAgentGateway();
    const tmpl = new MockTemplateGateway(agent);

    // Create a template and an agent from it (synchronized)
    const template = await tmpl.createTemplate({
      name: "T1",
      content: "# v1",
      defaultProfileId: "p1",
    });
    await tmpl.createAgentFromTemplate(PROJECT_ID, template.id, {
      name: "SyncedAgent",
      synchronized: true,
    });

    // Update the template to produce drift
    await tmpl.updateTemplate(template.id, "# v2 updated");

    // Render AgentsPanel — drift should be detected on mount
    renderAgentsPanel(agent, tmpl);
    await waitForAgentsIdle();

    // Badge should be visible
    await waitFor(() => {
      expect(screen.getByLabelText("update available")).toBeTruthy();
    });
    expect(screen.getByRole("button", { name: "sync SyncedAgent" })).toBeTruthy();
  });

  it("clicking Sync removes the 'update available' badge", async () => {
    const agent = new MockAgentGateway();
    const tmpl = new MockTemplateGateway(agent);

    const template = await tmpl.createTemplate({
      name: "T2",
      content: "# v1",
      defaultProfileId: "p1",
    });
    await tmpl.createAgentFromTemplate(PROJECT_ID, template.id, {
      name: "DriftedAgent",
      synchronized: true,
    });

    // Create drift
    await tmpl.updateTemplate(template.id, "# v2 content");

    renderAgentsPanel(agent, tmpl);
    await waitForAgentsIdle();

    // Badge should appear
    await waitFor(() => {
      expect(screen.getByLabelText("update available")).toBeTruthy();
    });

    // Click Sync
    fireEvent.click(screen.getByRole("button", { name: "sync DriftedAgent" }));

    // Badge disappears after sync
    await waitFor(() => {
      expect(screen.queryByLabelText("update available")).toBeNull();
    });

    // Verify drift is empty at the gateway level
    const drifts = await tmpl.detectDrift(PROJECT_ID);
    expect(drifts).toHaveLength(0);
  });

  it("does not show badge for non-synchronized agents", async () => {
    const agent = new MockAgentGateway();
    const tmpl = new MockTemplateGateway(agent);

    const template = await tmpl.createTemplate({
      name: "T3",
      content: "# v1",
      defaultProfileId: "p1",
    });
    // synchronized: false → no drift
    await tmpl.createAgentFromTemplate(PROJECT_ID, template.id, {
      name: "UnsyncedAgent",
      synchronized: false,
    });

    await tmpl.updateTemplate(template.id, "# v2 content");

    renderAgentsPanel(agent, tmpl);
    await waitForAgentsIdle();

    // Give it a moment to detect drift
    await waitFor(() => {
      expect(screen.queryByText("UnsyncedAgent")).toBeTruthy();
    });

    // No badge
    expect(screen.queryByLabelText("update available")).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// MockTemplateGateway unit tests
// ---------------------------------------------------------------------------

describe("MockTemplateGateway (unit)", () => {
  it("listTemplates returns empty initially", async () => {
    const gw = new MockTemplateGateway(new MockAgentGateway());
    expect(await gw.listTemplates()).toEqual([]);
  });

  it("createTemplate assigns sequential ids and version 1", async () => {
    const gw = new MockTemplateGateway(new MockAgentGateway());
    const t1 = await gw.createTemplate({ name: "A", content: "a", defaultProfileId: "" });
    const t2 = await gw.createTemplate({ name: "B", content: "b", defaultProfileId: "" });
    expect(t1.id).toBe("mock-template-1");
    expect(t2.id).toBe("mock-template-2");
    expect(t1.version).toBe(1);
    expect(t2.version).toBe(1);
  });

  it("updateTemplate increments version and stores new content", async () => {
    const gw = new MockTemplateGateway(new MockAgentGateway());
    const t = await gw.createTemplate({ name: "T", content: "old", defaultProfileId: "" });
    const updated = await gw.updateTemplate(t.id, "new content");
    expect(updated.version).toBe(2);
    expect(updated.contentMd).toBe("new content");

    // Idempotent list
    const list = await gw.listTemplates();
    expect(list[0].version).toBe(2);
  });

  it("updateTemplate throws NOT_FOUND for unknown template", async () => {
    const gw = new MockTemplateGateway(new MockAgentGateway());
    await expect(gw.updateTemplate("ghost", "x")).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("deleteTemplate removes the template", async () => {
    const gw = new MockTemplateGateway(new MockAgentGateway());
    const t = await gw.createTemplate({ name: "Del", content: "x", defaultProfileId: "" });
    await gw.deleteTemplate(t.id);
    expect(await gw.listTemplates()).toHaveLength(0);
  });

  it("deleteTemplate throws NOT_FOUND for unknown template", async () => {
    const gw = new MockTemplateGateway(new MockAgentGateway());
    await expect(gw.deleteTemplate("ghost")).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("createAgentFromTemplate creates an agent with fromTemplate origin", async () => {
    const agentGw = new MockAgentGateway();
    const tmplGw = new MockTemplateGateway(agentGw);
    const t = await tmplGw.createTemplate({ name: "Proto", content: "## ctx", defaultProfileId: "p1" });
    const agent = await tmplGw.createAgentFromTemplate("proj", t.id, {
      name: "Derived",
      synchronized: true,
    });
    expect(agent.origin.type).toBe("fromTemplate");
    if (agent.origin.type === "fromTemplate") {
      expect(agent.origin.templateId).toBe(t.id);
      expect(agent.origin.syncedTemplateVersion).toBe(1);
    }
    expect(agent.synchronized).toBe(true);
    expect(agent.name).toBe("Derived");

    // Agent appears in the shared agent gateway
    const agents = await agentGw.listAgents("proj");
    expect(agents).toHaveLength(1);
    expect(agents[0].id).toBe(agent.id);
  });

  it("detectDrift returns drift for synchronized agents with stale version", async () => {
    const agentGw = new MockAgentGateway();
    const tmplGw = new MockTemplateGateway(agentGw);
    const t = await tmplGw.createTemplate({ name: "T", content: "v1", defaultProfileId: "" });
    const agent = await tmplGw.createAgentFromTemplate("proj", t.id, {
      synchronized: true,
    });

    // No drift yet (versions match)
    expect(await tmplGw.detectDrift("proj")).toHaveLength(0);

    // Update template
    await tmplGw.updateTemplate(t.id, "v2");

    const drifts = await tmplGw.detectDrift("proj");
    expect(drifts).toHaveLength(1);
    expect(drifts[0]).toMatchObject({ agentId: agent.id, from: 1, to: 2 });
  });

  it("syncAgent updates syncedTemplateVersion and returns { synced: true, version }", async () => {
    const agentGw = new MockAgentGateway();
    const tmplGw = new MockTemplateGateway(agentGw);
    const t = await tmplGw.createTemplate({ name: "T", content: "v1", defaultProfileId: "" });
    const agent = await tmplGw.createAgentFromTemplate("proj", t.id, {
      synchronized: true,
    });
    await tmplGw.updateTemplate(t.id, "v2 content");

    const result = await tmplGw.syncAgent("proj", agent.id);
    expect(result).toEqual({ synced: true, version: 2 });

    // No more drift
    expect(await tmplGw.detectDrift("proj")).toHaveLength(0);

    // Agent context updated
    const ctx = await agentGw.readContext("proj", agent.id);
    expect(ctx).toBe("v2 content");
  });

  it("syncAgent returns { synced: false, version: null } for scratch agents", async () => {
    const agentGw = new MockAgentGateway();
    const tmplGw = new MockTemplateGateway(agentGw);
    const a = await agentGw.createAgent("proj", { name: "Scratch", profileId: "p" });
    const result = await tmplGw.syncAgent("proj", a.id);
    expect(result).toEqual({ synced: false, version: null });
  });

  it("createAgentFromTemplate throws NOT_FOUND for unknown template", async () => {
    const gw = new MockTemplateGateway(new MockAgentGateway());
    await expect(
      gw.createAgentFromTemplate("proj", "ghost-template"),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });
});
