import { describe, test, expect, mock, beforeEach } from "bun:test";
import {
  ReviewTicketPipeline,
  formatReviewJiraComment,
} from "@/pipeline/review-ticket-pipeline.js";
import { mockLogger } from "../_helpers/mock-logger.js";
import type { FixPipeline } from "@/pipeline/fix-pipeline.js";
import type { JiraClientImpl } from "@/jira/client.js";
import type { JiraTicketReviewJobPayload, TriageResult } from "@/types/index.js";
import type { SlackNotifier } from "@/slack/client.js";
import { resetTicketReviewProcessedStoreForTests } from "@/poller/ticket-review-processed.js";

function makeJob(overrides: Partial<JiraTicketReviewJobPayload> = {}): JiraTicketReviewJobPayload {
  return {
    jobKind: "jira_ticket_review",
    issueId: "100",
    issueKey: "SQDP-1",
    summary: "Ticket summary",
    triggeredByCommentId: "comment-1",
    ...overrides,
  };
}

function triage(overrides: Partial<TriageResult> & Pick<TriageResult, "verdict">): TriageResult {
  return {
    title: null,
    summary: "Agent summary",
    questions: null,
    raw: "raw-output",
    ...overrides,
  };
}

/** Bun's `mock().mock.calls` is typed as `[]`; narrow after assertions. */
function addCommentBodyFromFirstCall(fn: { mock: { calls: unknown[] } }): string {
  expect(fn.mock.calls.length).toBeGreaterThan(0);
  const first = fn.mock.calls[0] as unknown;
  expect(Array.isArray(first) && first.length >= 2).toBe(true);
  return (first as [unknown, string])[1];
}

describe("formatReviewJiraComment", () => {
  test("includes questions when provided", () => {
    const body = formatReviewJiraComment("NEEDS_INFO", "Sum", "Q1?");
    expect(body).toContain("Questions / gaps:");
    expect(body).toContain("Q1?");
  });

  test("appends Jira size footer when over maxCommentChars", () => {
    const longSummary = "x".repeat(500);
    const footer = "_(Remainder truncated for Jira comment size limit.)_";
    const body = formatReviewJiraComment("PROCEED", longSummary, null, 120);
    expect(body.length).toBeLessThanOrEqual(120);
    expect(body).toContain(footer);
    expect(body.endsWith(footer) || body.includes(footer)).toBe(true);
  });
});

describe("ReviewTicketPipeline.execute", () => {
  const addComment = mock(() => Promise.resolve());
  const evaluateCardReadiness = mock(async () => triage({ verdict: "PROCEED" }));

  const jiraClient = { addComment } as unknown as JiraClientImpl;
  const fixPipeline = { evaluateCardReadiness } as unknown as FixPipeline;

  beforeEach(() => {
    resetTicketReviewProcessedStoreForTests({ removePersistedFile: true });
    addComment.mockClear();
    evaluateCardReadiness.mockClear();
    evaluateCardReadiness.mockImplementation(async () => triage({ verdict: "PROCEED" }));
  });

  test("PROCEED → success, completed, Jira comment with readiness verdict", async () => {
    const pipeline = new ReviewTicketPipeline({
      jiraClient,
      fixPipeline,
      jiraDomain: "acme",
      logger: mockLogger(),
    });

    const result = await pipeline.execute(makeJob());
    expect(result.success).toBe(true);
    expect(result.outcome).toBe("completed");
    expect(addComment).toHaveBeenCalledTimes(1);
    const posted = addCommentBodyFromFirstCall(addComment);
    expect(posted).toContain("Ready to implement");
    expect(posted).toContain("Agent summary");
  });

  test("NEEDS_INFO → needs_info outcome", async () => {
    evaluateCardReadiness.mockImplementation(async () => triage({ verdict: "NEEDS_INFO" }));
    const pipeline = new ReviewTicketPipeline({
      jiraClient,
      fixPipeline,
      jiraDomain: "acme",
      logger: mockLogger(),
    });

    const result = await pipeline.execute(makeJob());
    expect(result.success).toBe(true);
    expect(result.outcome).toBe("needs_info");
  });

  test("RUN_FAILED → success false, run_failed, abortReason", async () => {
    evaluateCardReadiness.mockImplementation(async () =>
      triage({ verdict: "RUN_FAILED", summary: "cli died" }),
    );
    const pipeline = new ReviewTicketPipeline({
      jiraClient,
      fixPipeline,
      jiraDomain: "acme",
      logger: mockLogger(),
    });

    const result = await pipeline.execute(makeJob());
    expect(result.success).toBe(false);
    expect(result.outcome).toBe("run_failed");
    expect(result.abortReason).toBe("cli died");
    const posted = addCommentBodyFromFirstCall(addComment);
    expect(posted).toContain("RUN_FAILED");
  });

  test("notifyTicketReviewStarted failure is logged; pipeline still completes", async () => {
    const notifyTicketReviewStarted = mock(() => Promise.reject(new Error("slack down")));
    const notifyTicketReviewFinished = mock(() => Promise.resolve());
    const slackNotifier = {
      notifyTicketReviewStarted,
      notifyTicketReviewFinished,
    } as unknown as SlackNotifier;

    const pipeline = new ReviewTicketPipeline({
      jiraClient,
      fixPipeline,
      jiraDomain: "acme",
      slackNotifier,
      logger: mockLogger(),
    });

    const result = await pipeline.execute(makeJob());
    expect(result.success).toBe(true);
    expect(notifyTicketReviewStarted).toHaveBeenCalled();
    expect(notifyTicketReviewFinished).toHaveBeenCalled();
  });

  test("notifyTicketReviewFinished failure is logged; pipeline still completes", async () => {
    const notifyTicketReviewStarted = mock(() => Promise.resolve());
    const notifyTicketReviewFinished = mock(() => Promise.reject(new Error("slack finish failed")));
    const slackNotifier = {
      notifyTicketReviewStarted,
      notifyTicketReviewFinished,
    } as unknown as SlackNotifier;

    const pipeline = new ReviewTicketPipeline({
      jiraClient,
      fixPipeline,
      jiraDomain: "acme",
      slackNotifier,
      logger: mockLogger(),
    });

    const result = await pipeline.execute(makeJob());
    expect(result.success).toBe(true);
  });

  test("evaluateCardReadiness throws → failure comment + notifyTicketReviewFailed", async () => {
    evaluateCardReadiness.mockImplementation(async () => {
      throw new Error("boom");
    });
    const notifyTicketReviewFailed = mock(() => Promise.resolve());
    const slackNotifier = {
      notifyTicketReviewFailed,
    } as unknown as SlackNotifier;

    const pipeline = new ReviewTicketPipeline({
      jiraClient,
      fixPipeline,
      jiraDomain: "acme",
      slackNotifier,
      logger: mockLogger(),
    });

    const result = await pipeline.execute(makeJob());
    expect(result.success).toBe(false);
    expect(result.abortReason).toBe("boom");
    expect(addComment).toHaveBeenCalled();
    const failBody = addCommentBodyFromFirstCall(addComment);
    expect(failBody).toContain("Implementation readiness failed");
    expect(failBody).toContain("boom");
    expect(notifyTicketReviewFailed).toHaveBeenCalledWith(
      "SQDP-1",
      "https://acme.atlassian.net/browse/SQDP-1",
      "boom",
    );
  });

  test("addComment throws on failure path → still returns and logs", async () => {
    evaluateCardReadiness.mockImplementation(async () => {
      throw new Error("eval failed");
    });
    addComment.mockImplementation(() => Promise.reject(new Error("jira write failed")));

    const pipeline = new ReviewTicketPipeline({
      jiraClient,
      fixPipeline,
      jiraDomain: "acme",
      logger: mockLogger(),
    });

    const result = await pipeline.execute(makeJob());
    expect(result.success).toBe(false);
    expect(result.abortReason).toBe("eval failed");
  });
});
