import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { resetLaneConfig, setLaneConfig } from "@/lib/lane-config";
import { TEST_AGENT_TOKEN as mockToken, makeDispatchEnvMock, authedRequest } from "@/test/route-helpers";

process.env.DISPATCH_AGENT_TOKEN = mockToken;

vi.mock("@/lib/dispatch-env", () => makeDispatchEnvMock());

const { mocks } = vi.hoisted(() => ({
  mocks: {
    issueFindMany: vi.fn(),
    prFixFindMany: vi.fn(),
    findLeasedIssueIds: vi.fn(),
  },
}));

vi.mock("@/lib/prisma", () => ({
  prisma: {
    issue: { findMany: mocks.issueFindMany },
    prFixQueueItem: { findMany: mocks.prFixFindMany },
  },
  asPrFixQueueClient: (client: any) => client,
}));

vi.mock("@/lib/lease", () => ({
  findLeasedIssueIds: mocks.findLeasedIssueIds,
}));

import { GET } from "./route";
import { resetAuthCaches } from "@/lib/auth";

function request(url: string, agentName = "example-agent", includeAuth = true) {
  return authedRequest(`http://localhost${url}`, { includeAuth });
}

describe("GET /api/agents/[agentName]/queue", () => {
  beforeEach(() => {
    delete process.env.DISPATCH_AUTH_MODE;
    resetAuthCaches();
    vi.clearAllMocks();
    mocks.prFixFindMany.mockResolvedValue([]);
    mocks.issueFindMany.mockResolvedValue([]);
    mocks.findLeasedIssueIds.mockResolvedValue([]);
  });

  afterEach(() => {
    resetLaneConfig();
  });

  // ── Auth tests ─────────────────────────────────────────────────────

  it("returns 401 when no auth header is present", async () => {
    const res = await GET(
      request("/api/agents/example-agent/queue?lane=local", "example-agent", false),
      { params: Promise.resolve({ agentName: "example-agent" }) },
    );

    expect(res.status).toBe(401);
    const body = await res.json();
    expect(body.error).toBe("Unauthorized");
  });

  it("returns 401 for bad bearer token", async () => {
    const headers: Record<string, string> = { Authorization: "Bearer wrong-token" };
    const req = new Request("http://localhost/api/agents/example-agent/queue?lane=local", { headers });

    const res = await GET(req, {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(401);
    const body = await res.json();
    expect(body.error).toBe("Unauthorized");
  });

  it("valid bearer token preserves existing queue behavior", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-auth",
        number: 1,
        title: "Auth test issue",
        url: "https://github.com/org/repo/issues/1",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(
      request("/api/agents/example-agent/queue?lane=local"),
      { params: Promise.resolve({ agentName: "example-agent" }) },
    );

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
    expect(body[0].number).toBe(1);
  });

  it("unauthorized request does not call findLeasedIssueIds", async () => {
    const req = new Request("http://localhost/api/agents/example-agent/queue?lane=local", {
      headers: { Authorization: "Bearer wrong-token" },
    });

    await GET(req, { params: Promise.resolve({ agentName: "example-agent" }) });

    expect(mocks.findLeasedIssueIds).not.toHaveBeenCalled();
  });

  it("unauthorized request does not call PR-fix lookup", async () => {
    const req = new Request("http://localhost/api/agents/example-agent/queue?lane=local", {
      headers: { Authorization: "Bearer wrong-token" },
    });

    await GET(req, { params: Promise.resolve({ agentName: "example-agent" }) });

    expect(mocks.prFixFindMany).not.toHaveBeenCalled();
  });

  it("unauthorized request does not call issue lookup", async () => {
    const req = new Request("http://localhost/api/agents/example-agent/queue?lane=local", {
      headers: { Authorization: "Bearer wrong-token" },
    });

    await GET(req, { params: Promise.resolve({ agentName: "example-agent" }) });

    expect(mocks.issueFindMany).not.toHaveBeenCalled();
  });

  // ── PR review-fix prioritization ───────────────────────────────────

  it("prioritizes queued PR review-fix items before new issue work", async () => {
    mocks.prFixFindMany.mockResolvedValue([
      {
        id: "prfix-1",
        repo: "org/repo",
        pr: 12,
        issue: 67,
        branch: "fix/issue-67",
        url: "https://github.com/org/repo/pull/12",
        title: "Fix issue 67",
        lane: "NORMAL",
        status: "QUEUED",
        reason: "review changes requested",
        feedback: ["please update tests"],
        evidenceKeys: ["review:1"],
        author: "itsmiso-ai",
        queuedAt: new Date("2026-01-01T00:00:00Z"),
        updatedAt: new Date("2026-01-01T00:00:00Z"),
      },
    ]);
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-abc",
        number: 99,
        title: "Regular issue",
        url: "https://github.com/org/repo/issues/99",
        labels: ["priority/p0", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body[0]).toMatchObject({ type: "pr-review-fix", repo: "org/repo", pr: 12 });
    expect(body[1]).toMatchObject({ number: 99, title: "Regular issue" });
    expect(mocks.prFixFindMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ status: "QUEUED", lane: "NORMAL" }) }));
  });

  it("returns issueId and repoFullName on issue items", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-def",
        number: 50,
        title: "Another issue",
        url: "https://github.com/org/repo/issues/50",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body[0]).toHaveProperty("issueId", "issue-def");
    expect(body[0]).toHaveProperty("repoFullName", "org/repo");
  });

  it("returns type: issue on issue items", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-ghi",
        number: 51,
        title: "Test issue",
        url: "https://github.com/org/repo/issues/51",
        labels: ["priority/p1", "status/ready"],
        currentLane: "frontier",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=frontier"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body[0]).toHaveProperty("type", "issue");
  });

  it("keeps PR-fix items first when both types are present", async () => {
    mocks.prFixFindMany.mockResolvedValue([
      {
        id: "prfix-2",
        repo: "org/repo",
        pr: 20,
        issue: 10,
        branch: "fix/issue-10",
        url: "https://github.com/org/repo/pull/20",
        title: "Fix PR",
        lane: "NORMAL",
        status: "QUEUED",
        reason: "review requested",
        feedback: [],
        evidenceKeys: [],
        author: "bot",
        queuedAt: new Date("2026-01-02T00:00:00Z"),
        updatedAt: new Date("2026-01-02T00:00:00Z"),
      },
    ]);
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-jkl",
        number: 5,
        title: "Issue work",
        url: "https://github.com/org/repo/issues/5",
        labels: ["enhancement", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body[0].type).toBe("pr-review-fix");
    expect(body[1].type).toBe("issue");
  });

  // ── Agent assignment tests ─────────────────────────────────────────

  it("includes same-agent claimed issues by default and excludes unlabelled orphans", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-claimed",
        number: 52,
        title: "Claimed issue",
        url: "https://github.com/org/repo/issues/52",
        labels: ["agent/example-agent", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
      {
        id: "issue-unclaimed",
        number: 53,
        title: "Unclaimed issue",
        url: "https://github.com/org/repo/issues/53",
        labels: [],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.map((item: { number: number }) => item.number)).toEqual([52]);
  });

  it("includes claimed issues when includeClaimed=true", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-claimed",
        number: 52,
        title: "Claimed issue",
        url: "https://github.com/org/repo/issues/52",
        labels: ["agent/example-agent", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local&includeClaimed=true"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body[0]).toMatchObject({ number: 52, agentMatch: true });
  });

  // ── Lease-aware filtering tests (issue #166) ───────────────────────

  it("excludes issues leased by other agents", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-leased",
        number: 54,
        title: "Leased issue",
        url: "https://github.com/org/repo/issues/54",
        labels: ["agent/opencode", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
      {
        id: "issue-open",
        number: 55,
        title: "Open issue",
        url: "https://github.com/org/repo/issues/55",
        labels: ["bug", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);
    mocks.findLeasedIssueIds.mockResolvedValue(["issue-leased"]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.map((item: { number: number }) => item.number)).toEqual([55]);
  });

  it("includes issues when the requesting agent holds the lease", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-my-lease",
        number: 56,
        title: "My leased issue",
        url: "https://github.com/org/repo/issues/56",
        labels: ["agent/example-agent", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);
    mocks.findLeasedIssueIds.mockResolvedValue([]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local&includeClaimed=true"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.map((item: { number: number }) => item.number)).toEqual([56]);
  });

  it("calls findLeasedIssueIds with the requesting agent name", async () => {
    mocks.issueFindMany.mockResolvedValue([]);

    await GET(request("/api/agents/saffron/queue?lane=local", "saffron"), {
      params: Promise.resolve({ agentName: "saffron" }),
    });

    expect(mocks.findLeasedIssueIds).toHaveBeenCalledWith("saffron");
  });

  // ── Renovate exclusion tests ──────────────────────────────────────

  it("excludes Renovate issues from agent queue by default", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-renovate",
        number: 10,
        title: "Dependency Dashboard",
        url: "https://github.com/org/repo/issues/10",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
      {
        id: "issue-normal",
        number: 20,
        title: "Fix login bug",
        url: "https://github.com/org/repo/issues/20",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.map((item: { number: number }) => item.number)).toEqual([20]);
  });

  it("includes Renovate issues when includeRenovate=true", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-renovate",
        number: 10,
        title: "Dependency Dashboard",
        url: "https://github.com/org/repo/issues/10",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
      {
        id: "issue-normal",
        number: 20,
        title: "Fix login bug",
        url: "https://github.com/org/repo/issues/20",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local&includeRenovate=true"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    const numbers = body.map((item: { number: number }) => item.number);
    expect(numbers).toContain(10);
    expect(numbers).toContain(20);
  });

  it("excludes Renovate issues with renovate label by default", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-renovate",
        number: 10,
        title: "Bump lodash",
        url: "https://github.com/org/repo/issues/10",
        labels: ["renovate", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
      {
        id: "issue-normal",
        number: 20,
        title: "Fix crash",
        url: "https://github.com/org/repo/issues/20",
        labels: ["bug", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.map((item: { number: number }) => item.number)).toEqual([20]);
  });

  it("excludes Renovate issues with Update dependency title by default", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-renovate",
        number: 10,
        title: "Update dependency lodash to v4.18.0",
        url: "https://github.com/org/repo/issues/10",
        labels: [],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
      {
        id: "issue-normal",
        number: 20,
        title: "Add dark mode",
        url: "https://github.com/org/repo/issues/20",
        labels: ["enhancement", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.map((item: { number: number }) => item.number)).toEqual([20]);
  });

  it("does not exclude non-Renovate issues", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-normal",
        number: 20,
        title: "Update README",
        url: "https://github.com/org/repo/issues/20",
        labels: ["documentation", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.map((item: { number: number }) => item.number)).toEqual([20]);
  });

  // ── Lane validation tests ─────────────────────────────────────────

  it("returns 400 for unknown lane", async () => {
    mocks.issueFindMany.mockResolvedValue([]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=unknown-lane"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("Invalid lane");
    expect(body.error).toContain("unknown-lane");
  });

  it("returns 200 for default normal lane", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-normal",
        number: 1,
        title: "Normal issue",
        url: "https://github.com/org/repo/issues/1",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
    expect(body[0].number).toBe(1);
  });

  it("returns 200 for default escalated lane", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-escalated",
        number: 2,
        title: "Escalated issue",
        url: "https://github.com/org/repo/issues/2",
        labels: ["priority/p0", "status/ready"],
        currentLane: "frontier",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=frontier"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
    expect(body[0].number).toBe(2);
  });

  it("returns 200 when lane param is omitted (backward-compatible)", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-no-lane",
        number: 3,
        title: "No lane filter",
        url: "https://github.com/org/repo/issues/3",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
  });

  it("returns 200 for custom configured lane", async () => {
    setLaneConfig({
      lanes: [
        { id: "fast", title: "Fast Lane", claimable: true },
        { id: "slow", title: "Slow Lane", claimable: true },
        { id: "parked", title: "Parked", claimable: false },
      ],
    });

    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-fast",
        number: 10,
        title: "Fast issue",
        url: "https://github.com/org/repo/issues/10",
        labels: ["priority/p0", "status/ready"],
        currentLane: "fast",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=fast"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
    expect(body[0].number).toBe(10);
  });

  it("returns 400 for default lane when custom config is active", async () => {
    setLaneConfig({
      lanes: [
        { id: "fast", title: "Fast Lane", claimable: true },
        { id: "parked", title: "Parked", claimable: false },
      ],
    });

    mocks.issueFindMany.mockResolvedValue([]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("Invalid lane");
  });

  it("includes issue lane value in response", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-lane-val",
        number: 5,
        title: "Has lane",
        url: "https://github.com/org/repo/issues/5",
        labels: ["priority/p1", "status/ready"],
        currentLane: "frontier",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=frontier"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body[0].lane).toBe("frontier");
  });

  it("preserves includeClaimed behavior with lane filter", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-claimed-lane",
        number: 6,
        title: "Claimed in lane",
        url: "https://github.com/org/repo/issues/6",
        labels: ["agent/example-agent", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local&includeClaimed=true"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body[0]).toMatchObject({ number: 6, agentMatch: true });
  });

  it("preserves includeRenovate behavior with lane filter", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-renovate-lane",
        number: 7,
        title: "Dependency Dashboard",
        url: "https://github.com/org/repo/issues/7",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local&includeRenovate=true"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
    expect(body[0].number).toBe(7);
  });

  it("preserves exclude_decomposed behavior with lane filter", async () => {
    mocks.issueFindMany.mockResolvedValue([
      {
        id: "issue-decomposed",
        number: 8,
        title: "Decomposed issue",
        url: "https://github.com/org/repo/issues/8",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: true,
        repository: { fullName: "org/repo" },
      },
      {
        id: "issue-not-decomposed",
        number: 9,
        title: "Not decomposed",
        url: "https://github.com/org/repo/issues/9",
        labels: ["priority/p1", "status/ready"],
        currentLane: "local",
        decomposed: false,
        repository: { fullName: "org/repo" },
      },
    ]);

    const res = await GET(request("/api/agents/example-agent/queue?lane=local&exclude_decomposed=true"), {
      params: Promise.resolve({ agentName: "example-agent" }),
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
    expect(body[0].number).toBe(9);
  });
});
