import { env } from "@code/env/convex";
import { type TestConvex, convexTest } from "convex-test";
import { anyApi } from "convex/server";
import { describe, expect, test } from "vitest";

import { internal } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
import schema from "./schema";

declare global {
  interface ImportMeta {
    readonly glob: (pattern: string) => Record<string, () => Promise<unknown>>;
  }
}

const modules = import.meta.glob("./**/*.ts");
const api = anyApi;

const FLUE_DB_TOKEN = env.FLUE_DB_TOKEN;

const ID_A = "https://convex.test|user-a";
const identityA = { tokenIdentifier: ID_A };

const SOURCE = {
  host: "github.com",
  projectName: "zopu",
  repositoryPath: "puter/zopu",
  normalizedUrl: "https://github.com/puter/zopu",
  url: "https://github.com/puter/zopu",
};

const REMOTE = {
  defaultBranch: "main",
  documents: [
    {
      kind: "readme" as const,
      path: "README.md",
      content: "# zopu\n\nRepository: https://github.com/puter/zopu\n",
    },
  ],
  warnings: [],
};

const createTestProject = async (
  t: TestConvex<typeof schema>
): Promise<Id<"projects">> => {
  // Ensure personal organization for the user
  await t
    .withIdentity(identityA)
    .mutation(api.organizations.ensurePersonalOrganization);
  // Persist the public Git import
  const outcome = await t
    .withIdentity(identityA)
    .mutation(internal.projects.persistPublicGitImport, {
      userId: ID_A,
      source: SOURCE,
      remote: REMOTE,
    });
  return outcome.id as unknown as Id<"projects">;
};

const eventsForProject = async (
  t: TestConvex<typeof schema>,
  projectId: Id<"projects">
) => {
  return await t.query((ctx) =>
    ctx.db
      .query("projectEvents")
      .withIndex("by_project_and_createdAt", (q) =>
        q.eq("projectId", projectId)
      )
      .collect()
  );
};

describe("projectEvents", () => {
  test("project connect emits a project.connected event", async () => {
    const t = convexTest({ schema, modules });
    const projectId = await createTestProject(t);

    const events = await eventsForProject(t, projectId);
    expect(events).toHaveLength(1);
    expect(events[0].kind).toBe("project.connected");
    expect(events[0].projectId).toBe(projectId);
    expect(events[0].issueId).toBeUndefined();
    expect(events[0].runId).toBeUndefined();
  });

  test("issue create and begin emit issue.created then issue.queued events", async () => {
    const t = convexTest({ schema, modules });
    const projectId = await createTestProject(t);

    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "A representative issue body long enough to pass validation",
        projectId,
        title: "Representative issue",
      });

    const afterCreate = await eventsForProject(t, projectId);
    expect(afterCreate.map((e) => e.kind)).toEqual([
      "project.connected",
      "issue.created",
    ]);
    const created = afterCreate[1];
    expect(created.issueId).toBe(issueId);
    expect(created.data).toMatchObject({
      number: 1,
      title: "Representative issue",
    });

    const status = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.begin, {
        issueId,
      });
    expect(status).toBe("queued");

    const afterBegin = await eventsForProject(t, projectId);
    expect(afterBegin.map((e) => e.kind)).toEqual([
      "project.connected",
      "issue.created",
      "issue.queued",
    ]);
    expect(afterBegin[2].issueId).toBe(issueId);
    expect(afterBegin[2].data).toMatchObject({ number: 1 });
  });

  test("artifact update emits an artifact.updated event", async () => {
    const t = convexTest({ schema, modules });
    const projectId = await createTestProject(t);
    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "A representative issue body long enough to pass validation",
        projectId,
        title: "Representative issue",
      });

    const revision = await t.mutation(api.agentWorkspace.updateArtifact, {
      content: "# Updated work\n\nRevised content.",
      issueId,
      path: "work.md",
      token: FLUE_DB_TOKEN,
    });

    expect(revision).toBe(3);

    const events = await eventsForProject(t, projectId);
    const artifactEvent = events.find((e) => e.kind === "artifact.updated");
    expect(artifactEvent).toBeDefined();
    expect(artifactEvent?.issueId).toBe(issueId);
    expect(artifactEvent?.data).toMatchObject({ path: "work.md", revision: 3 });
  });

  test("agent status emits an agent.<status> event linked to the run", async () => {
    const t = convexTest({ schema, modules });
    const projectId = await createTestProject(t);
    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "A representative issue body long enough to pass validation",
        projectId,
        title: "Representative issue",
      });
    await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.begin, { issueId });

    const run = await t.mutation(api.agentWorkspace.ensureRun, {
      daemonId: "daemon-1",
      issueId,
      token: FLUE_DB_TOKEN,
    });
    expect(run).toMatchObject({
      baseBranch: "main",
      checkoutPath: "/workspace/repository",
      sourceUrl: SOURCE.url,
    });
    expect(run?.branchName).toMatch(/^work\/issue-1-/u);
    await t.mutation(api.agentWorkspace.setStatus, {
      issueId,
      status: "completed",
      summary: "Done.",
      token: FLUE_DB_TOKEN,
    });

    const events = await eventsForProject(t, projectId);
    const workspaceEvent = events.find(
      (e) => e.kind === "agent.workspace.created"
    );
    expect(workspaceEvent).toBeDefined();
    expect(workspaceEvent?.runId).toBeDefined();

    const statusEvent = events.find((e) => e.kind === "agent.completed");
    expect(statusEvent).toBeDefined();
    expect(statusEvent?.issueId).toBe(issueId);
    expect(statusEvent?.runId).toBe(workspaceEvent?.runId);
    expect(statusEvent?.data).toMatchObject({ summary: "Done." });
  });

  test("wrong agent token is rejected before any event is written", async () => {
    const t = convexTest({ schema, modules });
    const projectId = await createTestProject(t);
    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "A representative issue body long enough to pass validation",
        projectId,
        title: "Representative issue",
      });

    await expect(
      t.mutation(api.agentWorkspace.setStatus, {
        issueId,
        status: "completed",
        summary: "Done.",
        token: "wrong-token",
      })
    ).rejects.toThrow(/Invalid agent control token/u);

    const events = await eventsForProject(t, projectId);
    expect(events.some((e) => e.kind === "agent.completed")).toBe(false);
  });

  test("Gitea lifecycle persists PR metadata in an event and artifact", async () => {
    const t = convexTest({ schema, modules });
    const projectId = await createTestProject(t);
    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "A representative issue body long enough to pass validation",
        projectId,
        title: "Representative issue",
      });
    await t.mutation(api.agentWorkspace.ensureRun, {
      daemonId: "daemon-1",
      issueId,
      token: FLUE_DB_TOKEN,
    });

    await t.mutation(api.agentWorkspace.recordGiteaLifecycle, {
      baseBranch: "main",
      branch: "work/issue-1",
      commitSha: "abc123",
      issueId,
      pullRequest: {
        baseBranch: "main",
        branch: "work/issue-1",
        number: 5,
        status: "open",
        url: "https://git.openputer.com/puter/zopu-code/pulls/5",
      },
      status: "pull_request_open",
      token: FLUE_DB_TOKEN,
    });

    const events = await eventsForProject(t, projectId);
    const event = events.find(
      (item) => item.kind === "gitea.pull_request.created"
    );
    expect(event?.data).toMatchObject({
      branch: "work/issue-1",
      commitSha: "abc123",
      pullRequest: {
        number: 5,
        status: "open",
      },
      status: "pull_request_open",
    });

    const artifacts = await t.query((ctx) =>
      ctx.db
        .query("projectArtifacts")
        .withIndex("by_project_and_path", (q) =>
          q.eq("projectId", projectId).eq("path", "artifacts.md")
        )
        .unique()
    );
    expect(artifacts?.content).toContain(
      "https://git.openputer.com/puter/zopu-code/pulls/5"
    );
  });
});
