import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";
import { describe, expect, test } from "vitest";

import {
  buildActivityTimeline,
  buildWorkUnitCard,
  buildWorkUnitDetail,
  eventsForIssue,
  extractArtifactPaths,
  extractLatestSummary,
  extractPullRequestFromEvents,
} from "./work-unit-projection";
import type { LinkedSignal } from "./work-unit-projection";

type Issue = Doc<"projectIssues">;
type Event = Doc<"projectEvents">;

const ISSUE_A = "issue-a" as Id<"projectIssues">;
const ISSUE_B = "issue-b" as Id<"projectIssues">;

const makeLinkedSignal = (
  overrides: Partial<LinkedSignal> = {}
): LinkedSignal => ({
  createdAt: 5000,
  signalId: "sig-1" as Id<"signals">,
  sourceCount: 2,
  summary: "Three users hit the same error.",
  title: "OAuth crash on Safari",
  ...overrides,
});

const makeIssue = (overrides: Partial<Issue> = {}): Issue =>
  ({
    _creationTime: 1,
    _id: ISSUE_A,
    body: "Fix the Safari OAuth callback so users can complete sign-in.",
    createdAt: 1000,
    number: 1,
    projectId: "project-1" as Id<"projects">,
    status: "open",
    title: "Fix Safari OAuth callback",
    updatedAt: 2000,
    ...overrides,
  }) as Issue;

const makeEvent = (
  kind: string,
  data: Record<string, unknown>,
  overrides: Partial<Event> = {}
): Event =>
  ({
    _creationTime: 1,
    _id: `evt-${kind}-${overrides.createdAt ?? 1000}` as Id<"projectEvents">,
    createdAt: overrides.createdAt ?? 1000,
    data,
    issueId: ISSUE_A,
    kind,
    projectId: "project-1" as Id<"projects">,
    ...overrides,
  }) as Event;

describe("eventsForIssue", () => {
  test("filters to a specific issue and ignores undefined", () => {
    const events: Event[] = [
      makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
      makeEvent(
        "issue.created",
        { number: 2 },
        { createdAt: 2000, issueId: ISSUE_B }
      ),
    ];
    expect(eventsForIssue(events, ISSUE_A)).toHaveLength(1);
    expect(eventsForIssue(undefined, ISSUE_A)).toEqual([]);
  });
});

describe("extractArtifactPaths", () => {
  test("collects distinct paths from artifact.updated events", () => {
    const events: Event[] = [
      makeEvent(
        "artifact.updated",
        { path: "work.md", revision: 1 },
        { createdAt: 1000 }
      ),
      makeEvent(
        "artifact.updated",
        { path: "work.md", revision: 2 },
        { createdAt: 2000 }
      ),
      makeEvent(
        "artifact.updated",
        { path: "artifacts.md", revision: 1 },
        { createdAt: 3000 }
      ),
      makeEvent("issue.created", { number: 1 }, { createdAt: 4000 }),
    ];
    expect(extractArtifactPaths(events)).toEqual(["artifacts.md", "work.md"]);
  });

  test("returns empty for no artifact events", () => {
    expect(extractArtifactPaths([])).toEqual([]);
  });
});

describe("buildWorkUnitCard", () => {
  test("projects a fresh issue with no events and no linked signals", () => {
    const card = buildWorkUnitCard({
      issue: makeIssue(),
      issueEvents: [],
      linkedSignals: [],
    });
    expect(card.title).toBe("Fix Safari OAuth callback");
    expect(card.status).toBe("open");
    expect(card.stepCount).toBe(0);
    expect(card.currentActivity).toBeNull();
    expect(card.needsInput).toBe(false);
    expect(card.hasPullRequest).toBe(false);
    expect(card.artifactCount).toBe(0);
    expect(card.signalCount).toBe(0);
    expect(card.summary).toContain("Fix the Safari OAuth");
  });

  test("counts per-issue artifacts from events, not project-wide", () => {
    const events: Event[] = [
      makeEvent(
        "artifact.updated",
        { path: "work.md", revision: 1 },
        { createdAt: 1000 }
      ),
      makeEvent(
        "artifact.updated",
        { path: "artifacts.md", revision: 1 },
        { createdAt: 2000 }
      ),
    ];
    const card = buildWorkUnitCard({
      issue: makeIssue({ status: "working" }),
      issueEvents: events,
      linkedSignals: [],
    });
    expect(card.artifactCount).toBe(2);
    expect(card.stepCount).toBe(2);
  });

  test("reflects needs-input status and activity from events", () => {
    const events = [
      makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
      makeEvent("issue.queued", { number: 1 }, { createdAt: 2000 }),
      makeEvent("agent.needs-input", {}, { createdAt: 3000 }),
    ];
    const card = buildWorkUnitCard({
      issue: makeIssue({ status: "needs-input" }),
      issueEvents: events,
      linkedSignals: [],
    });
    expect(card.needsInput).toBe(true);
    expect(card.stepCount).toBe(3);
    expect(card.currentActivity).toBe("Needs your input");
  });

  test("detects PR from gitea event", () => {
    const events = [
      makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
      makeEvent(
        "gitea.pull_request.created",
        {
          pullRequest: { number: 42, url: "https://git.example.com/pulls/42" },
        },
        { createdAt: 5000 }
      ),
    ];
    const card = buildWorkUnitCard({
      issue: makeIssue({ status: "completed" }),
      issueEvents: events,
      linkedSignals: [],
    });
    expect(card.hasPullRequest).toBe(true);
  });
});

describe("linked signal projection", () => {
  test("card signalCount reflects real linked signals", () => {
    const card = buildWorkUnitCard({
      issue: makeIssue(),
      issueEvents: [],
      linkedSignals: [
        makeLinkedSignal(),
        makeLinkedSignal({
          signalId: "sig-2" as Id<"signals">,
          title: "Build error",
        }),
      ],
    });
    expect(card.signalCount).toBe(2);
  });

  test("card signalCount is 0 when no signals are linked", () => {
    const card = buildWorkUnitCard({
      issue: makeIssue(),
      issueEvents: [],
      linkedSignals: [],
    });
    expect(card.signalCount).toBe(0);
  });

  test("detail exposes linked signals with title, summary, and source count", () => {
    const detail = buildWorkUnitDetail({
      issue: makeIssue(),
      issueEvents: [],
      linkedSignals: [
        makeLinkedSignal(),
        makeLinkedSignal({
          signalId: "sig-2" as Id<"signals">,
          sourceCount: 5,
          summary: "CI broke",
        }),
      ],
    });
    expect(detail.signalCount).toBe(2);
    expect(detail.linkedSignals).toHaveLength(2);
    expect(detail.linkedSignals[0].title).toBe("OAuth crash on Safari");
    expect(detail.linkedSignals[0].sourceCount).toBe(2);
    expect(detail.linkedSignals[1].sourceCount).toBe(5);
  });

  test("detail linked signals empty when no attachment exists", () => {
    const detail = buildWorkUnitDetail({
      issue: makeIssue(),
      issueEvents: [],
      linkedSignals: [],
    });
    expect(detail.signalCount).toBe(0);
    expect(detail.linkedSignals).toEqual([]);
  });
});

describe("extractLatestSummary", () => {
  test("returns the most recent agent summary", () => {
    const events = [
      makeEvent("agent.working", {}, { createdAt: 1000 }),
      makeEvent(
        "agent.completed",
        { summary: "All tests passed." },
        { createdAt: 5000 }
      ),
    ];
    expect(extractLatestSummary(events)).toBe("All tests passed.");
  });

  test("returns null when no terminal agent event exists", () => {
    const events = [
      makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
    ];
    expect(extractLatestSummary(events)).toBeNull();
  });

  test("prefers failure error over earlier completion", () => {
    const events = [
      makeEvent("agent.completed", { summary: "Done." }, { createdAt: 1000 }),
      makeEvent(
        "agent.failed",
        { error: "Tests failed." },
        { createdAt: 5000 }
      ),
    ];
    expect(extractLatestSummary(events)).toBe("Tests failed.");
  });
});

describe("extractPullRequestFromEvents", () => {
  test("extracts URL and number from gitea event", () => {
    const events = [
      makeEvent(
        "gitea.pull_request.created",
        { pullRequest: { number: 7, url: "https://git.example.com/pulls/7" } },
        { createdAt: 1000 }
      ),
    ];
    const info = extractPullRequestFromEvents(events);
    expect(info.hasPR).toBe(true);
    expect(info.url).toBe("https://git.example.com/pulls/7");
    expect(info.number).toBe(7);
  });

  test("returns false when no PR event exists", () => {
    expect(extractPullRequestFromEvents([]).hasPR).toBe(false);
  });
});

describe("buildActivityTimeline", () => {
  test("sorts newest first and maps labels", () => {
    const events = [
      makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
      makeEvent("agent.completed", { summary: "Done." }, { createdAt: 5000 }),
    ];
    const timeline = buildActivityTimeline(events);
    expect(timeline).toHaveLength(2);
    expect(timeline[0].kind).toBe("agent.completed");
    expect(timeline[0].label).toBe("Work completed");
    expect(timeline[1].kind).toBe("issue.created");
    expect(timeline[1].label).toBe("Work created");
  });

  test("maps signal.attached event to activity", () => {
    const events = [
      makeEvent(
        "signal.attached",
        { signalId: "sig-1", signalProblemTitle: "OAuth crash" },
        { createdAt: 3000 }
      ),
    ];
    const timeline = buildActivityTimeline(events);
    expect(timeline[0].label).toBe("Signal linked");
    expect(timeline[0].detail).toContain("OAuth crash");
  });
});

describe("buildWorkUnitDetail", () => {
  test("aggregates all detail fields from per-issue events", () => {
    const events: Event[] = [
      makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
      makeEvent(
        "artifact.updated",
        { path: "work.md", revision: 2 },
        { createdAt: 2000 }
      ),
      makeEvent(
        "artifact.updated",
        { path: "artifacts.md", revision: 1 },
        { createdAt: 2500 }
      ),
      makeEvent(
        "gitea.pull_request.created",
        { pullRequest: { number: 3, url: "https://git.example.com/pulls/3" } },
        { createdAt: 3000 }
      ),
    ];
    const detail = buildWorkUnitDetail({
      issue: makeIssue({ status: "completed" }),
      issueEvents: events,
      linkedSignals: [makeLinkedSignal()],
    });
    expect(detail.stepCount).toBe(4);
    expect(detail.artifactCount).toBe(2);
    expect(detail.artifactPaths).toEqual(["artifacts.md", "work.md"]);
    expect(detail.signalCount).toBe(1);
    expect(detail.hasPullRequest).toBe(true);
    expect(detail.pullRequestUrl).toBe("https://git.example.com/pulls/3");
    expect(detail.pullRequestNumber).toBe(3);
    expect(detail.activity).toHaveLength(4);
    expect(detail.activity[0].kind).toBe("gitea.pull_request.created");
  });
});

// ---------------------------------------------------------------------------
// Cross-issue isolation: one issue must never inherit another issue's
// artifacts, PR, signals, or activity.
// ---------------------------------------------------------------------------

describe("cross-issue isolation", () => {
  const SIGNAL_A = makeLinkedSignal({ signalId: "sig-a" as Id<"signals"> });
  const SIGNAL_B = makeLinkedSignal({
    signalId: "sig-b" as Id<"signals">,
    title: "Different signal",
  });

  const sharedEvents: Event[] = [
    makeEvent(
      "issue.created",
      { number: 1 },
      { createdAt: 1000, issueId: ISSUE_A }
    ),
    makeEvent(
      "artifact.updated",
      { path: "work.md", revision: 1 },
      { createdAt: 2000, issueId: ISSUE_A }
    ),
    makeEvent(
      "gitea.pull_request.created",
      { pullRequest: { number: 5, url: "https://git.example.com/pulls/5" } },
      { createdAt: 3000, issueId: ISSUE_A }
    ),
    makeEvent(
      "agent.completed",
      { summary: "Done for A." },
      { createdAt: 4000, issueId: ISSUE_A }
    ),
    // Issue B events — completely separate
    makeEvent(
      "issue.created",
      { number: 2 },
      { createdAt: 1500, issueId: ISSUE_B }
    ),
    makeEvent(
      "artifact.updated",
      { path: "artifacts.md", revision: 1 },
      { createdAt: 2500, issueId: ISSUE_B }
    ),
    makeEvent(
      "agent.failed",
      { error: "Failed for B." },
      { createdAt: 3500, issueId: ISSUE_B }
    ),
  ];

  test("issue A does not inherit issue B's artifacts", () => {
    const eventsA = eventsForIssue(sharedEvents, ISSUE_A);
    const cardA = buildWorkUnitCard({
      issue: makeIssue({ _id: ISSUE_A, number: 1 }),
      issueEvents: eventsA,
      linkedSignals: [SIGNAL_A],
    });
    expect(cardA.artifactCount).toBe(1);
    expect(cardA.stepCount).toBe(4);

    const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
    const cardB = buildWorkUnitCard({
      issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
      issueEvents: eventsB,
      linkedSignals: [SIGNAL_B],
    });
    expect(cardB.artifactCount).toBe(1);
    expect(cardB.stepCount).toBe(3);
  });

  test("issue A does not inherit issue B's PR", () => {
    const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
    const cardB = buildWorkUnitCard({
      issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
      issueEvents: eventsB,
      linkedSignals: [],
    });
    expect(cardB.hasPullRequest).toBe(false);
  });

  test("issue A does not inherit issue B's summary", () => {
    const eventsA = eventsForIssue(sharedEvents, ISSUE_A);
    expect(extractLatestSummary(eventsA)).toBe("Done for A.");

    const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
    expect(extractLatestSummary(eventsB)).toBe("Failed for B.");
  });

  test("detail for issue A lists only issue A's artifact paths", () => {
    const eventsA = eventsForIssue(sharedEvents, ISSUE_A);
    const detailA = buildWorkUnitDetail({
      issue: makeIssue({ _id: ISSUE_A, number: 1 }),
      issueEvents: eventsA,
      linkedSignals: [SIGNAL_A],
    });
    expect(detailA.artifactPaths).toEqual(["work.md"]);
    expect(detailA.pullRequestNumber).toBe(5);
  });

  test("detail for issue B lists only issue B's artifact paths and no PR", () => {
    const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
    const detailB = buildWorkUnitDetail({
      issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
      issueEvents: eventsB,
      linkedSignals: [],
    });
    expect(detailB.artifactPaths).toEqual(["artifacts.md"]);
    expect(detailB.hasPullRequest).toBe(false);
    expect(detailB.pullRequestUrl).toBeNull();
  });

  test("issue A does not inherit issue B's linked signals", () => {
    const detailA = buildWorkUnitDetail({
      issue: makeIssue({ _id: ISSUE_A, number: 1 }),
      issueEvents: eventsForIssue(sharedEvents, ISSUE_A),
      linkedSignals: [SIGNAL_A],
    });
    expect(detailA.signalCount).toBe(1);
    expect(detailA.linkedSignals[0].title).toBe("OAuth crash on Safari");

    const detailB = buildWorkUnitDetail({
      issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
      issueEvents: eventsForIssue(sharedEvents, ISSUE_B),
      linkedSignals: [SIGNAL_B],
    });
    expect(detailB.signalCount).toBe(1);
    expect(detailB.linkedSignals[0].title).toBe("Different signal");
  });
});
