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 TOKEN = env.FLUE_DB_TOKEN;
const BAD_TOKEN = "not-the-right-token";

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

const newTest = () => convexTest({ schema, modules });

const ensureOrg = async (t: TestConvex<typeof schema>) => {
  const org = await t
    .withIdentity(identityA)
    .mutation(api.organizations.ensurePersonalOrganization, {});
  return org._id as Id<"organizations">;
};

const problemStatement = {
  title: "Deploy fails on staging",
  summary: "The staging deploy command exits with a DNS resolution error.",
  desiredOutcome: "Staging deploys successfully without DNS errors.",
  constraints: ["Must not change the production pipeline"],
};

const seedMessage = async (
  t: TestConvex<typeof schema>,
  orgId: Id<"organizations">,
  clientRequestId: string,
  rawText: string
): Promise<string> => {
  const begun = await t
    .withIdentity(identityA)
    .mutation(api.conversationMessages.beginUserMessage, {
      clientRequestId,
      organizationId: orgId,
      rawText,
    });
  await t
    .withIdentity(identityA)
    .mutation(api.conversationMessages.markAdmitted, {
      clientRequestId,
      organizationId: orgId,
      submissionId: `sub-${clientRequestId}`,
    });
  return begun.messageId;
};

const createProject = async (
  t: TestConvex<typeof schema>,
  ownerId: string,
  repoName: string
): Promise<Id<"projects">> => {
  const ownerIdentity = { tokenIdentifier: ownerId };
  await t
    .withIdentity(ownerIdentity)
    .mutation(api.organizations.ensurePersonalOrganization);
  const outcome = await t
    .withIdentity(ownerIdentity)
    .mutation(internal.projects.persistPublicGitImport, {
      userId: ownerId,
      source: {
        host: "github.com",
        projectName: repoName,
        repositoryPath: `owner/${repoName}`,
        normalizedUrl: `https://github.com/owner/${repoName}`,
        url: `https://github.com/owner/${repoName}`,
      },
      remote: {
        defaultBranch: "main",
        documents: [
          {
            kind: "readme" as const,
            path: "README.md",
            content: `# ${repoName}\n\nTest.\n`,
          },
        ],
        warnings: [],
      },
    });
  return outcome.id as unknown as Id<"projects">;
};

// Create a project-scoped signal via the routing backend.
const createRoutingSignal = async (
  t: TestConvex<typeof schema>,
  orgId: Id<"organizations">,
  projectId: Id<"projects">,
  messageIds: string[]
): Promise<Id<"signals">> => {
  const result = await t.mutation(api.signalRouting.createSignal, {
    messageIds,
    organizationId: orgId,
    problemStatement,
    processedByAgentInstanceId: orgId,
    projectId,
    token: TOKEN,
  });
  return result.signalId as Id<"signals">;
};

describe("signalRouting authentication", () => {
  test("invalid token is rejected on every function", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "auth-test");
    const messageId = await seedMessage(t, orgId, "req-auth", "test message");

    await expect(
      t.mutation(api.signalRouting.createSignal, {
        messageIds: [messageId],
        organizationId: orgId,
        problemStatement,
        processedByAgentInstanceId: orgId,
        projectId,
        token: BAD_TOKEN,
      })
    ).rejects.toThrow(/Invalid agent control token/u);

    await expect(
      t.query(api.signalRouting.listEvidence, {
        organizationId: orgId,
        token: BAD_TOKEN,
      })
    ).rejects.toThrow(/Invalid agent control token/u);

    await expect(
      t.query(api.signalRouting.listActiveIssues, {
        organizationId: orgId,
        projectId,
        token: BAD_TOKEN,
      })
    ).rejects.toThrow(/Invalid agent control token/u);

    await expect(
      t.query(api.signalRouting.listProjects, {
        organizationId: orgId,
        token: BAD_TOKEN,
      })
    ).rejects.toThrow(/Invalid agent control token/u);
  });
});

describe("signalRouting create and route", () => {
  test("create signal, list evidence, and list signals", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "route-test");
    const messageId = await seedMessage(
      t,
      orgId,
      "req-route",
      "The work-unit card should expose the generated PR."
    );

    // Evidence appears before signal creation.
    const evidenceBefore = await t.query(api.signalRouting.listEvidence, {
      organizationId: orgId,
      token: TOKEN,
    });
    expect(evidenceBefore).toHaveLength(1);
    expect(evidenceBefore[0]?.rawText).toBe(
      "The work-unit card should expose the generated PR."
    );

    // Create the signal.
    const signalId = await createRoutingSignal(t, orgId, projectId, [
      messageId,
    ]);

    // Evidence is consumed after signal creation.
    const evidenceAfter = await t.query(api.signalRouting.listEvidence, {
      organizationId: orgId,
      token: TOKEN,
    });
    expect(evidenceAfter).toHaveLength(0);

    // Signal appears in the project list.
    const signals = await t.query(api.signalRouting.listSignals, {
      organizationId: orgId,
      projectId,
      token: TOKEN,
    });
    expect(signals).toHaveLength(1);
    expect(signals[0]?._id).toBe(signalId);
    expect(signals[0]?.problemStatement.title).toBe("Deploy fails on staging");
  });

  test("attach signal to existing issue", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "attach-test");
    const messageId = await seedMessage(t, orgId, "req-attach", "problem");
    const signalId = await createRoutingSignal(t, orgId, projectId, [
      messageId,
    ]);

    // Create an issue via the existing projectIssues backend.
    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "Existing issue body that is long enough.",
        projectId,
        title: "Existing UI issue",
      });

    const result = await t.mutation(api.signalRouting.attachSignalToIssue, {
      issueId: issueId as string,
      organizationId: orgId,
      signalId: signalId as string,
      token: TOKEN,
    });

    expect(result.alreadyAttached).toBe(false);

    // List active issues includes the attached issue.
    const issues = await t.query(api.signalRouting.listActiveIssues, {
      organizationId: orgId,
      projectId,
      token: TOKEN,
    });
    expect(issues).toHaveLength(1);
    expect(issues[0]?._id).toBe(issueId);
  });

  test("create issue from signal (attach-vs-create fallback)", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "create-test");
    const messageId = await seedMessage(t, orgId, "req-create", "new problem");
    const signalId = await createRoutingSignal(t, orgId, projectId, [
      messageId,
    ]);

    const result = await t.mutation(api.signalRouting.createIssueFromSignal, {
      organizationId: orgId,
      signalId: signalId as string,
      token: TOKEN,
    });

    expect(result.created).toBe(true);
    expect(result.projectId).toBe(projectId);

    // The issue exists and is open.
    const issues = await t.query(api.signalRouting.listActiveIssues, {
      organizationId: orgId,
      projectId,
      token: TOKEN,
    });
    expect(issues).toHaveLength(1);
    expect(issues[0]?.title).toBe("Deploy fails on staging");
  });
});

describe("signalRouting idempotency", () => {
  test("duplicate attach retry returns existing without duplicate event", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "idem-attach");
    const messageId = await seedMessage(t, orgId, "req-idem-att", "problem");
    const signalId = await createRoutingSignal(t, orgId, projectId, [
      messageId,
    ]);

    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "Existing issue body that is long enough for validation.",
        projectId,
        title: "Pre-existing issue",
      });

    // First attach.
    const first = await t.mutation(api.signalRouting.attachSignalToIssue, {
      issueId: issueId as string,
      organizationId: orgId,
      signalId: signalId as string,
      token: TOKEN,
    });
    expect(first.alreadyAttached).toBe(false);

    // Retry the same attach.
    const second = await t.mutation(api.signalRouting.attachSignalToIssue, {
      issueId: issueId as string,
      organizationId: orgId,
      signalId: signalId as string,
      token: TOKEN,
    });
    expect(second.alreadyAttached).toBe(true);
    expect(second.attachmentId).toBe(first.attachmentId);

    // Verify only one signal.attached event was emitted (not duplicated).
    const events = await t
      .withIdentity(identityA)
      .query(api.projectIssues.events, { projectId });
    const attached = events.filter((e: { kind: string }) =>
      e.kind.includes("signal.attached")
    );
    expect(attached).toHaveLength(1);
  });

  test("duplicate create-from-signal retry returns existing issue", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "idem-create");
    const messageId = await seedMessage(t, orgId, "req-idem-crt", "problem");
    const signalId = await createRoutingSignal(t, orgId, projectId, [
      messageId,
    ]);

    // First create.
    const first = await t.mutation(api.signalRouting.createIssueFromSignal, {
      organizationId: orgId,
      signalId: signalId as string,
      token: TOKEN,
    });
    expect(first.created).toBe(true);

    // Retry — should return existing, not create a new issue.
    const second = await t.mutation(api.signalRouting.createIssueFromSignal, {
      organizationId: orgId,
      signalId: signalId as string,
      token: TOKEN,
    });
    expect(second.created).toBe(false);
    expect(second.issueId).toBe(first.issueId);

    // Only one issue exists.
    const issues = await t.query(api.signalRouting.listActiveIssues, {
      organizationId: orgId,
      projectId,
      token: TOKEN,
    });
    expect(issues).toHaveLength(1);
  });

  test("duplicate signal creation is idempotent (sourceKey)", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "idem-signal");
    const messageId = await seedMessage(t, orgId, "req-idem-sig", "test");

    const first = await t.mutation(api.signalRouting.createSignal, {
      messageIds: [messageId],
      organizationId: orgId,
      problemStatement,
      processedByAgentInstanceId: orgId,
      projectId,
      token: TOKEN,
    });
    const second = await t.mutation(api.signalRouting.createSignal, {
      messageIds: [messageId],
      organizationId: orgId,
      problemStatement,
      processedByAgentInstanceId: orgId,
      projectId,
      token: TOKEN,
    });

    expect(second.signalId).toBe(first.signalId);
  });
});

describe("signalRouting cross-project rejection", () => {
  test("attach rejects when signal and issue are in different projects", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectA = await createProject(t, ID_A, "cross-a");
    const projectB = await createProject(t, ID_A, "cross-b");

    const messageId = await seedMessage(t, orgId, "req-cross", "problem");
    // Signal is scoped to project A.
    const signalId = await createRoutingSignal(t, orgId, projectA, [messageId]);

    // Issue is in project B.
    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "Issue in project B with enough body text.",
        projectId: projectB,
        title: "Project B issue",
      });

    await expect(
      t.mutation(api.signalRouting.attachSignalToIssue, {
        issueId: issueId as string,
        organizationId: orgId,
        signalId: signalId as string,
        token: TOKEN,
      })
    ).rejects.toThrow(/must belong to the same project/u);
  });

  test("attach rejects org-scoped signal (no projectId)", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "org-scope-test");

    const messageId = await seedMessage(t, orgId, "req-org", "problem");

    // Create an org-scoped signal (no projectId).
    const signalId = await t.mutation(api.signalRouting.createSignal, {
      messageIds: [messageId],
      organizationId: orgId,
      problemStatement,
      processedByAgentInstanceId: orgId,
      token: TOKEN,
    });

    const issueId = await t
      .withIdentity(identityA)
      .mutation(api.projectIssues.create, {
        body: "Issue body that is long enough for the validator.",
        projectId,
        title: "Some issue",
      });

    await expect(
      t.mutation(api.signalRouting.attachSignalToIssue, {
        issueId: issueId as string,
        organizationId: orgId,
        signalId: signalId.signalId as string,
        token: TOKEN,
      })
    ).rejects.toThrow(/not project-scoped/u);
  });
});

describe("signalRouting provenance", () => {
  test("exact message raw text is preserved through the routing loop", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "prov-test");
    const rawText = "  The work-unit card should expose the generated PR.  ";
    const messageId = await seedMessage(t, orgId, "req-prov", rawText);

    // Evidence shows exact raw text.
    const evidence = await t.query(api.signalRouting.listEvidence, {
      organizationId: orgId,
      token: TOKEN,
    });
    expect(evidence[0]?.rawText).toBe(rawText);

    // Signal creation preserves exact snapshot.
    const signalId = await createRoutingSignal(t, orgId, projectId, [
      messageId,
    ]);

    // Verify via the existing signals.get that the snapshot is exact.
    const composed = await t
      .withIdentity(identityA)
      .query(api.signals.get, { signalId });
    expect(composed?.sources[0]?.rawTextSnapshot).toBe(rawText);
  });
});

describe("signalRouting begin issue", () => {
  test("begin transitions an open issue to queued", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "begin-test");
    const messageId = await seedMessage(t, orgId, "req-begin", "start this");
    const signalId = await createRoutingSignal(t, orgId, projectId, [
      messageId,
    ]);

    const result = await t.mutation(api.signalRouting.createIssueFromSignal, {
      organizationId: orgId,
      signalId: signalId as string,
      token: TOKEN,
    });

    const status = await t.mutation(api.signalRouting.beginIssue, {
      issueId: result.issueId as string,
      organizationId: orgId,
      token: TOKEN,
    });

    expect(status).toBe("queued");
  });

  test("begin rejects invalid token", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "begin-auth");
    const messageId = await seedMessage(t, orgId, "req-begin-auth", "msg");
    const signalId = await createRoutingSignal(t, orgId, projectId, [
      messageId,
    ]);

    const result = await t.mutation(api.signalRouting.createIssueFromSignal, {
      organizationId: orgId,
      signalId: signalId as string,
      token: TOKEN,
    });

    await expect(
      t.mutation(api.signalRouting.beginIssue, {
        issueId: result.issueId as string,
        organizationId: orgId,
        token: BAD_TOKEN,
      })
    ).rejects.toThrow(/Invalid agent control token/u);
  });
});

describe("signalRouting project context", () => {
  test("get project context returns project and documents", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "ctx-test");

    const context = await t.query(api.signalRouting.getProjectContext, {
      organizationId: orgId,
      projectId,
      token: TOKEN,
    });

    expect(context.project._id).toBe(projectId);
    expect(context.project.name).toBe("ctx-test");
    expect(context.contextDocuments.length).toBeGreaterThan(0);
  });

  test("list projects returns organization projects", async () => {
    const t = newTest();
    const orgId = await ensureOrg(t);
    const projectId = await createProject(t, ID_A, "list-proj-test");

    const projects = await t.query(api.signalRouting.listProjects, {
      organizationId: orgId,
      token: TOKEN,
    });

    expect(projects).toHaveLength(1);
    expect(projects[0]?._id).toBe(projectId);
  });
});
