import { parsePRComments } from "./pr-comment-parser.js";
import { execFileSync } from "child_process";
jest.mock("child_process", () => ({
    execFileSync: jest.fn(),
}));
const mockedExecFileSync = execFileSync;
// ---------------------------------------------------------------------------
// Fixture helpers — build PR comment bodies matching the real renderReport()
// output wrapped in the progress comment from testbot/src/progress.ts.
// ---------------------------------------------------------------------------
function progressWrapper(reportBody) {
    return `### Skyramp Testbot Plan
Reviewing the Pull Request for test recommendations.

- [x] Analyzing code changes
- [x] Running tests
- [x] Generating report

${reportBody}`;
}
/**
 * Realistic full report from commit 1 of a PR.
 * Matches the exact markdown format produced by renderReport() in testbot.
 */
const FULL_REPORT_COMMIT_1 = progressWrapper(`### 📋 Business Case Analysis
This PR adds CRUD endpoints for the items resource and an orders endpoint.

### 💡 New Tests Created
- **Integration** for POST/GET/PUT/DELETE /api/v1/items/ — \`test_items_integration.py\`
  Tests full CRUD lifecycle on items
  📎 Scenario: \`tests/scenario_crud_items.json\`
- **Contract** for GET /api/v1/items/{item_id} — \`test_items_contract.py\`
  Validates response schema for single item retrieval
- **Fuzz** for POST /api/v1/items/ — \`test_items_fuzz.py\`
  Sends malformed payloads to item creation

### 🧪 Test Results
| Test Type | Endpoint | Status | Details |
|-----------|----------|--------|---------|
| Integration | POST/GET/PUT/DELETE /api/v1/items/ | Pass | All 4 assertions passed |
| Contract | GET /api/v1/items/{item_id} | Fail | Schema mismatch on price field |
| Fuzz | POST /api/v1/items/ | Pass | 50 payloads, 0 crashes |

### 📌 Additional Recommendations (3)
<details>
<summary>Expand to see recommended tests not generated in this run</summary>

#### Integration

**\`order-lifecycle\`**

Integration test for order creation workflow

1. \`POST /api/v1/items/\` — Create item
2. \`POST /api/v1/orders/\` — Create order referencing item
3. \`GET /api/v1/orders/{order_id}\` — Verify order details

#### Fuzz

**\`fuzz-orders\`**

Fuzz test for order creation

1. \`POST /api/v1/orders/\` — Send malformed order payloads

#### E2E

**\`checkout-flow\`**

End-to-end checkout flow requiring Playwright traces

1. \`POST /api/v1/cart/\` — Add item to cart
2. \`POST /api/v1/checkout/\` — Complete checkout

</details>

### ⚠️ Issues Found
- Item price field returns string instead of number`);
/**
 * Minimal report: no tests generated, no results, only business case + recommendations.
 */
const MINIMAL_REPORT_NO_TESTS = progressWrapper(`### 📋 Business Case Analysis
Frontend-only PR. No backend changes detected.

### 📌 Additional Recommendations (2)
<details>
<summary>Expand to see recommended tests not generated in this run</summary>

#### E2E

**\`login-flow\`**

E2E test for login page

1. \`POST /api/v1/auth/login\` — Authenticate user

#### UI

**\`dashboard-layout\`**

UI test for dashboard responsiveness

</details>`);
/**
 * Report with maintenance results and collapsed sections.
 */
const REPORT_WITH_MAINTENANCE = progressWrapper(`<details>
<summary>📋 Business Case Analysis</summary>

Minor endpoint rename from /products to /items.

</details>

<details>
<summary>💡 New Tests Created</summary>

- **Contract** for GET /api/v1/items/ — \`test_items_contract.py\`
  Validates list items response schema

</details>

<details>
<summary>✅ Test Maintenance</summary>

| File | Change | Before | After |
|------|--------|--------|-------|
| \`test_products_smoke.py\` | Updated endpoint /products → /items | Fail (404 Not Found) | Pass (200 OK) |

</details>

<details>
<summary>🧪 Test Results</summary>

| Test Type | Endpoint | Status | Details |
|-----------|----------|--------|---------|
| Contract | GET /api/v1/items/ | Pass | Schema valid |
| Smoke | GET /api/v1/items/ | Pass | After maintenance fix |

</details>`);
// ---------------------------------------------------------------------------
// Helper to build a gh CLI response (array of PR comments)
// ---------------------------------------------------------------------------
function ghResponse(comments) {
    return JSON.stringify(comments.map((c) => ({
        id: c.id,
        body: c.body,
        user: { login: c.login ?? "github-actions[bot]" },
        created_at: new Date().toISOString(),
    })));
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
beforeEach(() => {
    mockedExecFileSync.mockReset();
});
describe("parsePRComments — error handling", () => {
    it("returns empty context when PR has no comments", async () => {
        mockedExecFileSync.mockReturnValue("[]");
        const ctx = await parsePRComments("owner", "repo", 1);
        expect(ctx.previousRecommendations).toEqual([]);
        expect(ctx.implementedTestFiles).toEqual([]);
        expect(ctx.executionResults).toEqual([]);
    });
    it("returns empty context when no TestBot comments exist", async () => {
        mockedExecFileSync.mockReturnValue(ghResponse([
            { id: 1, body: "LGTM!", login: "reviewer" },
            { id: 2, body: "Please fix the typo", login: "reviewer" },
        ]));
        const ctx = await parsePRComments("owner", "repo", 42);
        expect(ctx.previousRecommendations).toEqual([]);
        expect(ctx.implementedTestFiles).toEqual([]);
        expect(ctx.executionResults).toEqual([]);
    });
    it("uses only the latest TestBot comment when multiple exist", async () => {
        const olderReport = progressWrapper(`### 📋 Business Case Analysis
Old report.

### 💡 New Tests Created
- **Smoke** for GET /api/v1/old/ — \`test_old_smoke.py\``);
        mockedExecFileSync.mockReturnValue(ghResponse([
            { id: 100, body: olderReport },
            { id: 200, body: FULL_REPORT_COMMIT_1 },
        ]));
        const ctx = await parsePRComments("owner", "repo", 42);
        expect(ctx.implementedTestFiles).not.toContain("test_old_smoke.py");
        expect(ctx.implementedTestFiles).toContain("test_items_integration.py");
    });
    it("returns empty context when gh CLI fails", async () => {
        mockedExecFileSync.mockImplementation(() => {
            throw new Error("gh: command not found");
        });
        const ctx = await parsePRComments("owner", "repo", 42);
        expect(ctx.previousRecommendations).toEqual([]);
    });
    it("returns empty context when gh returns invalid JSON", async () => {
        mockedExecFileSync.mockReturnValue("not valid json{");
        const ctx = await parsePRComments("owner", "repo", 42);
        expect(ctx.previousRecommendations).toEqual([]);
    });
    it("returns empty context when gh returns non-array JSON", async () => {
        mockedExecFileSync.mockReturnValue('{"error": "not found"}');
        const ctx = await parsePRComments("owner", "repo", 42);
        expect(ctx.previousRecommendations).toEqual([]);
    });
    it("handles gh CLI timeout gracefully", async () => {
        mockedExecFileSync.mockImplementation(() => {
            const err = new Error("Command timed out");
            err.status = null;
            throw err;
        });
        const ctx = await parsePRComments("owner", "repo", 42);
        expect(ctx.previousRecommendations).toEqual([]);
    });
});
describe("full report parsing (commit 1 — non-collapsed)", () => {
    let ctx;
    beforeAll(async () => {
        mockedExecFileSync.mockReturnValue(ghResponse([{ id: 1, body: FULL_REPORT_COMMIT_1 }]));
        ctx = await parsePRComments("owner", "repo", 42);
    });
    it("extracts implemented recommendations from New Tests Created", () => {
        const implemented = ctx.previousRecommendations.filter((r) => r.status === "implemented");
        // 3 test entries: Integration (4 methods), Contract (1), Fuzz (1) = at least 6
        expect(implemented.length).toBeGreaterThanOrEqual(3);
    });
    it("expands POST/GET/PUT/DELETE into separate entries", () => {
        const implemented = ctx.previousRecommendations.filter((r) => r.status === "implemented");
        const methods = implemented
            .filter((r) => r.endpoint.includes("/api/v1/items/") && r.testType === "integration")
            .map((r) => r.endpoint.split(" ")[0]);
        expect(methods).toContain("POST");
        expect(methods).toContain("GET");
        expect(methods).toContain("PUT");
        expect(methods).toContain("DELETE");
    });
    it("assigns correct test types via proximity matching", () => {
        const implemented = ctx.previousRecommendations.filter((r) => r.status === "implemented");
        const fuzzEntry = implemented.find((r) => r.endpoint === "POST /api/v1/items/" && r.testType === "fuzz");
        expect(fuzzEntry).toBeDefined();
        const contractEntry = implemented.find((r) => r.endpoint.includes("/api/v1/items/{item_id}") && r.testType === "contract");
        expect(contractEntry).toBeDefined();
    });
    it("extracts recommended endpoints from Additional Recommendations", () => {
        const recommended = ctx.previousRecommendations.filter((r) => r.status === "recommended");
        expect(recommended.length).toBeGreaterThanOrEqual(1);
        const orderEndpoints = recommended.filter((r) => r.endpoint.includes("/api/v1/orders/"));
        expect(orderEndpoints.length).toBeGreaterThanOrEqual(1);
    });
    it("extracts scenario names from Additional Recommendations", () => {
        const recommended = ctx.previousRecommendations.filter((r) => r.status === "recommended");
        const orderLifecycle = recommended.find((r) => r.scenarioName === "order-lifecycle");
        expect(orderLifecycle).toBeDefined();
        expect(orderLifecycle?.testType).toBe("integration");
        const fuzzOrders = recommended.find((r) => r.scenarioName === "fuzz-orders");
        expect(fuzzOrders).toBeDefined();
    });
    it("extracts implemented file names only from New Tests Created", () => {
        expect(ctx.implementedTestFiles).toContain("test_items_integration.py");
        expect(ctx.implementedTestFiles).toContain("test_items_contract.py");
        expect(ctx.implementedTestFiles).toContain("test_items_fuzz.py");
        expect(ctx.implementedTestFiles).toHaveLength(3);
    });
    it("sets prNumber correctly", () => {
        expect(ctx.prNumber).toBe(42);
    });
});
describe("minimal report parsing (no tests generated)", () => {
    let ctx;
    beforeAll(async () => {
        mockedExecFileSync.mockReturnValue(ghResponse([{ id: 5, body: MINIMAL_REPORT_NO_TESTS }]));
        ctx = await parsePRComments("owner", "repo", 10);
    });
    it("has no implemented recommendations", () => {
        const implemented = ctx.previousRecommendations.filter((r) => r.status === "implemented");
        expect(implemented).toEqual([]);
    });
    it("has no implemented files", () => {
        expect(ctx.implementedTestFiles).toEqual([]);
    });
    it("extracts recommended endpoints from Additional Recommendations", () => {
        const recommended = ctx.previousRecommendations.filter((r) => r.status === "recommended");
        expect(recommended.length).toBeGreaterThanOrEqual(1);
        const loginRec = recommended.find((r) => r.endpoint.includes("/api/v1/auth/login"));
        expect(loginRec).toBeDefined();
    });
    it("extracts scenario names", () => {
        const recommended = ctx.previousRecommendations.filter((r) => r.status === "recommended");
        expect(recommended.find((r) => r.scenarioName === "login-flow")).toBeDefined();
        // dashboard-layout has no endpoint pattern — parser only captures recs with endpoints
        expect(recommended.find((r) => r.scenarioName === "dashboard-layout")).toBeUndefined();
    });
    it("has no execution results", () => {
        expect(ctx.executionResults).toEqual([]);
    });
});
describe("collapsed report parsing (details/summary tags)", () => {
    let ctx;
    beforeAll(async () => {
        mockedExecFileSync.mockReturnValue(ghResponse([{ id: 10, body: REPORT_WITH_MAINTENANCE }]));
        ctx = await parsePRComments("owner", "repo", 99);
    });
    it("extracts implemented tests from collapsed sections", () => {
        const implemented = ctx.previousRecommendations.filter((r) => r.status === "implemented");
        expect(implemented.length).toBeGreaterThanOrEqual(1);
        const contractEntry = implemented.find((r) => r.endpoint.includes("/api/v1/items/"));
        expect(contractEntry).toBeDefined();
    });
    it("extracts implemented file names from collapsed sections", () => {
        expect(ctx.implementedTestFiles).toContain("test_items_contract.py");
    });
    it("does not extract maintenance files as implemented test files", () => {
        expect(ctx.implementedTestFiles).not.toContain("test_products_smoke.py");
    });
});
describe("edge cases", () => {
    it("parses POST/GET without trailing backtick in path", async () => {
        const report = progressWrapper(`### 💡 New Tests Created
- **Integration** for POST/GET /api/v1/products/ — \`test_products_integration.py\``);
        mockedExecFileSync.mockReturnValue(ghResponse([{ id: 1, body: report }]));
        const ctx = await parsePRComments("owner", "repo", 1);
        const implemented = ctx.previousRecommendations.filter((r) => r.status === "implemented");
        const paths = implemented.map((r) => r.endpoint);
        expect(paths).toContain("POST /api/v1/products/");
        expect(paths).toContain("GET /api/v1/products/");
        for (const p of paths) {
            expect(p).not.toContain("`");
        }
    });
    it("handles path params like {item_id}", async () => {
        const report = progressWrapper(`### 💡 New Tests Created
- **Contract** for GET /api/v1/items/{item_id} — \`test_get_item_contract.py\``);
        mockedExecFileSync.mockReturnValue(ghResponse([{ id: 1, body: report }]));
        const ctx = await parsePRComments("owner", "repo", 1);
        const impl = ctx.previousRecommendations.filter((r) => r.status === "implemented");
        expect(impl).toEqual(expect.arrayContaining([
            expect.objectContaining({ endpoint: "GET /api/v1/items/{item_id}" }),
        ]));
    });
    it("de-duplicates implemented file names", async () => {
        const report = progressWrapper(`### 💡 New Tests Created
- **Integration** for POST /api/v1/items/ — \`test_items_integration.py\`
- **Contract** for GET /api/v1/items/ — \`test_items_integration.py\``);
        mockedExecFileSync.mockReturnValue(ghResponse([{ id: 1, body: report }]));
        const ctx = await parsePRComments("owner", "repo", 1);
        const count = ctx.implementedTestFiles.filter((f) => f === "test_items_integration.py").length;
        expect(count).toBe(1);
    });
    it("handles empty TestBot comment body", async () => {
        mockedExecFileSync.mockReturnValue(ghResponse([{ id: 1, body: "Skyramp Testbot — empty run" }]));
        const ctx = await parsePRComments("owner", "repo", 1);
        expect(ctx.previousRecommendations).toEqual([]);
        expect(ctx.implementedTestFiles).toEqual([]);
        expect(ctx.executionResults).toEqual([]);
    });
    it("ignores non-bot comments interspersed with bot comments", async () => {
        const botComment = progressWrapper(`### 💡 New Tests Created
- **Smoke** for GET /health — \`test_health_smoke.py\``);
        mockedExecFileSync.mockReturnValue(ghResponse([
            { id: 1, body: "Looks good to me!", login: "reviewer" },
            { id: 2, body: botComment },
            { id: 3, body: "Can you add more tests?", login: "reviewer" },
        ]));
        const ctx = await parsePRComments("owner", "repo", 1);
        expect(ctx.implementedTestFiles).toContain("test_health_smoke.py");
    });
    it("ignores human comments that mention bot markers", async () => {
        const humanMentioningBot = "The Skyramp Testbot output above looks wrong, can you rerun?";
        const realBotComment = progressWrapper(`### 💡 New Tests Created
- **Contract** for GET /api/v1/items/ — \`test_items_contract.py\``);
        mockedExecFileSync.mockReturnValue(ghResponse([
            { id: 1, body: realBotComment },
            { id: 2, body: humanMentioningBot, login: "reviewer" },
        ]));
        const ctx = await parsePRComments("owner", "repo", 1);
        expect(ctx.implementedTestFiles).toContain("test_items_contract.py");
        expect(ctx.previousRecommendations.length).toBeGreaterThanOrEqual(1);
    });
    it("ignores all comments when only a human mentions bot markers", async () => {
        const humanMentioningBot = "Hey team, check the Skyramp Testbot Plan from last time";
        mockedExecFileSync.mockReturnValue(ghResponse([
            { id: 1, body: humanMentioningBot, login: "developer123" },
        ]));
        const ctx = await parsePRComments("owner", "repo", 1);
        expect(ctx.previousRecommendations).toEqual([]);
        expect(ctx.implementedTestFiles).toEqual([]);
    });
});
describe("roundtrip: renderReport format → parser extraction", () => {
    it("parses the exact format from testbot report.test.ts validReport", async () => {
        // Mirrors the validReport fixture from testbot/src/__tests__/report.test.ts
        // rendered through renderReport() non-collapsed
        const renderedReport = `### 📋 Business Case Analysis
Tests cover the checkout flow.

### 💡 New Tests Created
- **contract** for POST /orders — \`test_orders_contract.py\`

### ✅ Test Maintenance
- Updated auth header in existing tests

### 🧪 Test Results
| Test Type | Endpoint | Status | Details |
|-----------|----------|--------|---------|
| contract | POST /orders | PASS | All assertions passed |
| fuzz | GET /products | FAIL | Unexpected 500 |

### ⚠️ Issues Found
- Server returns 500 on empty query param`;
        mockedExecFileSync.mockReturnValue(ghResponse([{ id: 1, body: progressWrapper(renderedReport) }]));
        const ctx = await parsePRComments("owner", "repo", 1);
        // Implemented: contract for POST /orders
        const impl = ctx.previousRecommendations.filter((r) => r.status === "implemented");
        expect(impl).toEqual(expect.arrayContaining([
            expect.objectContaining({
                testType: "contract",
                endpoint: "POST /orders",
                status: "implemented",
            }),
        ]));
        // File name from New Tests section only
        expect(ctx.implementedTestFiles).toContain("test_orders_contract.py");
        // No Additional Recommendations section → no recommended entries
        const recommended = ctx.previousRecommendations.filter((r) => r.status === "recommended");
        expect(recommended).toEqual([]);
    });
});
