/**
 * Bug Condition Exploration Property Test: Ivanti Queue Clear Completed FK Violation
 *
 * Spec: .kiro/specs/ivanti-queue-clear-completed-fix/ (bugfix)
 *
 * BUG CONDITION (from design.md):
 *   isBugCondition(input) returns true when linkedItems.length > 0
 *   — completed queue items have associated rows in jira_ticket_queue_items
 *
 * The current DELETE /completed handler issues a bare:
 *   DELETE FROM ivanti_todo_queue WHERE user_id = $1 AND status = 'complete'
 * which fails with a FK violation when child rows exist in jira_ticket_queue_items.
 *
 * THIS TEST ENCODES THE EXPECTED (FIXED) BEHAVIOR:
 *   The handler should delete junction table references first, then delete
 *   completed queue items, all within a transaction, and return success.
 *
 * ON UNFIXED CODE, THIS TEST WILL FAIL:
 *   The current handler does not use transactions or clean up junction rows.
 *   When pool.query receives the bare DELETE and junction rows exist, the mock
 *   simulates the FK violation error that PostgreSQL would throw.
 *   The handler catches the error and returns 500 instead of the expected 200.
 *
 * COUNTEREXAMPLE:
 *   "DELETE FROM ivanti_todo_queue fails with FK violation when junction rows
 *    reference completed items"
 *
 * **Validates: Requirements 1.1**
 */

const http = require('http');
const express = require('express');
const fc = require('fast-check');

// --- Mocks (must be installed BEFORE requiring the route module) ---

jest.mock('../middleware/auth', () => ({
    requireAuth: () => (req, _res, next) => {
        req.user = { id: 42, username: 'testuser', group: 'Admin' };
        next();
    },
    requireGroup: () => (_req, _res, next) => next(),
}));

jest.mock('../helpers/auditLog', () => jest.fn());

// Programmable pg pool mock
let queryHandler = () => Promise.resolve({ rows: [], rowCount: 0 });

const mockClient = {
    query: jest.fn((text, params) => queryHandler(text, params)),
    release: jest.fn(),
};

const mockPool = {
    query: jest.fn((text, params) => queryHandler(text, params)),
    connect: jest.fn(() => Promise.resolve(mockClient)),
};

jest.mock('../db', () => mockPool);

const createIvantiTodoQueueRouter = require('../routes/ivantiTodoQueue');

// --- HTTP helper ---

function request(server, method, urlPath) {
    return new Promise((resolve, reject) => {
        const addr = server.address();
        const options = {
            hostname: '127.0.0.1',
            port: addr.port,
            path: urlPath,
            method,
            headers: { 'Content-Type': 'application/json' },
        };
        const req = http.request(options, (res) => {
            const chunks = [];
            res.on('data', (chunk) => chunks.push(chunk));
            res.on('end', () => {
                const raw = Buffer.concat(chunks).toString();
                let body;
                try { body = JSON.parse(raw); } catch { body = raw; }
                resolve({ statusCode: res.statusCode, body });
            });
        });
        req.on('error', reject);
        req.end();
    });
}

// --- Generators ---

/**
 * Generate a non-empty array of completed queue item IDs (simulating items
 * that belong to the authenticated user and have status = 'complete').
 */
const completedItemIdsArb = fc.array(
    fc.integer({ min: 1, max: 10000 }),
    { minLength: 1, maxLength: 10 }
).map(ids => [...new Set(ids)]); // ensure unique IDs

/**
 * Generate junction table links for a subset of completed items.
 * At least one item MUST have a junction link (this is the bug condition).
 */
const junctionLinksArb = (itemIds) => {
    // Generate at least 1 junction link, up to 3 per item
    return fc.array(
        fc.record({
            jira_ticket_id: fc.integer({ min: 1, max: 5000 }),
            queue_item_id: fc.constantFrom(...itemIds),
        }),
        { minLength: 1, maxLength: Math.min(itemIds.length * 3, 15) }
    );
};

// --- Test Suite ---

describe('Bug Condition Exploration: FK Violation on Clear Completed With Junction Table Links', () => {
    let app;
    let server;

    beforeAll((done) => {
        app = express();
        app.use(express.json());
        app.use('/api/ivanti/todo-queue', createIvantiTodoQueueRouter());
        server = app.listen(0, '127.0.0.1', done);
    });

    afterAll((done) => {
        server.close(done);
    });

    beforeEach(() => {
        jest.clearAllMocks();
        mockClient.query.mockReset();
        mockClient.release.mockReset();
        mockPool.query.mockReset();
        mockPool.connect.mockReset();
        mockPool.connect.mockResolvedValue(mockClient);
        queryHandler = () => Promise.resolve({ rows: [], rowCount: 0 });
    });

    /**
     * Property 1: Bug Condition - FK Violation on Clear Completed With Junction Table Links
     *
     * For any set of completed queue items where at least one has junction table links,
     * the FIXED handler should:
     *   1. Use a transaction (BEGIN/COMMIT)
     *   2. Delete junction table references first
     *   3. Delete the completed queue items
     *   4. Return 200 with { message, deleted: N }
     *
     * On UNFIXED code: The handler uses a bare pool.query DELETE which will receive
     * a FK violation error from our mock (simulating PostgreSQL behavior), causing
     * the handler to return 500. This test FAILS, confirming the bug exists.
     *
     * **Validates: Requirements 1.1**
     */
    it('Property 1: completed items with junction links are deleted successfully (encodes expected fixed behavior)', async () => {
        await fc.assert(
            fc.asyncProperty(
                completedItemIdsArb,
                fc.context(),
                async (itemIds, ctx) => {
                    // Generate junction links for these items
                    const junctionLinks = itemIds.map(id => ({
                        jira_ticket_id: id * 10,
                        queue_item_id: id,
                    }));

                    ctx.log(`Testing with ${itemIds.length} completed items, ${junctionLinks.length} junction links`);
                    ctx.log(`Item IDs: ${JSON.stringify(itemIds)}`);

                    // Reset mocks for this iteration
                    mockClient.query.mockReset();
                    mockClient.release.mockReset();
                    mockPool.query.mockReset();
                    mockPool.connect.mockReset();
                    mockPool.connect.mockResolvedValue(mockClient);

                    // Configure the query handler to simulate the bug condition:
                    // - If the code uses pool.query with a bare DELETE on ivanti_todo_queue,
                    //   simulate the FK violation error (this is what the UNFIXED code does)
                    // - If the code uses a transaction (BEGIN, SELECT, DELETE junction, DELETE queue, COMMIT),
                    //   simulate successful execution (this is what the FIXED code should do)
                    const fkViolationError = new Error(
                        'update or delete on table "ivanti_todo_queue" violates foreign key constraint ' +
                        '"jira_ticket_queue_items_queue_item_id_fkey" on table "jira_ticket_queue_items"'
                    );
                    fkViolationError.code = '23503'; // PostgreSQL FK violation error code

                    // Handler for pool.query (unfixed code path)
                    mockPool.query.mockImplementation((text, params) => {
                        // The unfixed code issues a bare DELETE — simulate FK violation
                        if (text.includes('DELETE FROM ivanti_todo_queue')) {
                            return Promise.reject(fkViolationError);
                        }
                        return Promise.resolve({ rows: [], rowCount: 0 });
                    });

                    // Handler for client.query (fixed code path — transaction-based)
                    mockClient.query.mockImplementation((text, params) => {
                        if (text === 'BEGIN') {
                            return Promise.resolve({ rows: [], rowCount: 0 });
                        }
                        if (text.includes('SELECT') && text.includes('ivanti_todo_queue') && text.includes('complete')) {
                            return Promise.resolve({
                                rows: itemIds.map(id => ({ id })),
                                rowCount: itemIds.length,
                            });
                        }
                        if (text.includes('DELETE FROM jira_ticket_queue_items')) {
                            return Promise.resolve({
                                rows: [],
                                rowCount: junctionLinks.length,
                            });
                        }
                        if (text.includes('DELETE FROM ivanti_todo_queue')) {
                            return Promise.resolve({
                                rows: [],
                                rowCount: itemIds.length,
                            });
                        }
                        if (text === 'COMMIT') {
                            return Promise.resolve({ rows: [], rowCount: 0 });
                        }
                        if (text === 'ROLLBACK') {
                            return Promise.resolve({ rows: [], rowCount: 0 });
                        }
                        return Promise.resolve({ rows: [], rowCount: 0 });
                    });

                    // Make the request
                    const res = await request(server, 'DELETE', '/api/ivanti/todo-queue/completed');

                    // Assert EXPECTED (fixed) behavior:
                    // The handler should return 200 with the correct deleted count
                    expect(res.statusCode).toBe(200);
                    expect(res.body.message).toBe('Completed items cleared.');
                    expect(res.body.deleted).toBe(itemIds.length);
                }
            ),
            { numRuns: 20, verbose: 2 }
        );
    });
});


// --- Preservation Property Tests ---

/**
 * Property 2: Preservation - Clear Completed Without Junction Table Links
 *
 * Spec: .kiro/specs/ivanti-queue-clear-completed-fix/ (bugfix)
 *
 * PRESERVATION GOAL:
 *   Verify that the UNFIXED code already handles non-bug-condition cases correctly.
 *   These tests establish a baseline that must be preserved after the fix is applied.
 *
 * NON-BUG-CONDITION CASES:
 *   - Completed items WITHOUT junction table links → DELETE succeeds, returns count
 *   - No completed items exist → returns { deleted: 0 }
 *   - Pending/in-progress items → never touched by the DELETE
 *
 * The current handler uses:
 *   pool.query("DELETE FROM ivanti_todo_queue WHERE user_id = $1 AND status = 'complete'", [req.user.id])
 * This works correctly when no FK violations occur (no junction table references).
 *
 * EXPECTED OUTCOME: All tests PASS on unfixed code.
 *
 * **Validates: Requirements 3.1, 3.2, 3.3**
 */

describe('Preservation: Clear Completed Without Junction Table Links', () => {
    let app;
    let server;

    beforeAll((done) => {
        app = express();
        app.use(express.json());
        app.use('/api/ivanti/todo-queue', createIvantiTodoQueueRouter());
        server = app.listen(0, '127.0.0.1', done);
    });

    afterAll((done) => {
        server.close(done);
    });

    beforeEach(() => {
        jest.clearAllMocks();
        mockClient.query.mockReset();
        mockClient.release.mockReset();
        mockPool.query.mockReset();
        mockPool.connect.mockReset();
        mockPool.connect.mockResolvedValue(mockClient);
        queryHandler = () => Promise.resolve({ rows: [], rowCount: 0 });
    });

    /**
     * Property 2a: Completed items without junction links are all deleted
     *
     * For any random count of completed items (1–20) belonging to the user,
     * when none have junction table links, the DELETE succeeds and returns
     * the correct count.
     *
     * The fixed code uses a transaction (client.query) so we mock client.query
     * to simulate successful execution without FK violations.
     *
     * **Validates: Requirements 3.1**
     */
    it('Property 2a: completed items without junction links are deleted and correct count returned', async () => {
        await fc.assert(
            fc.asyncProperty(
                fc.integer({ min: 1, max: 20 }),
                fc.context(),
                async (completedCount, ctx) => {
                    ctx.log(`Testing with ${completedCount} completed items (no junction links)`);

                    // Reset mocks for this iteration
                    mockClient.query.mockReset();
                    mockClient.release.mockReset();
                    mockPool.connect.mockReset();
                    mockPool.connect.mockResolvedValue(mockClient);

                    // Generate item IDs
                    const itemIds = Array.from({ length: completedCount }, (_, i) => i + 1);

                    // Mock client.query for the transaction-based handler
                    mockClient.query.mockImplementation((text, params) => {
                        if (text === 'BEGIN') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text.includes('SELECT') && text.includes('ivanti_todo_queue') && text.includes('complete')) {
                            return Promise.resolve({ rows: itemIds.map(id => ({ id })), rowCount: completedCount });
                        }
                        if (text.includes('DELETE FROM jira_ticket_queue_items')) {
                            return Promise.resolve({ rows: [], rowCount: 0 }); // no junction links
                        }
                        if (text.includes('DELETE FROM ivanti_todo_queue')) {
                            return Promise.resolve({ rows: [], rowCount: completedCount });
                        }
                        if (text === 'COMMIT') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text === 'ROLLBACK') return Promise.resolve({ rows: [], rowCount: 0 });
                        return Promise.resolve({ rows: [], rowCount: 0 });
                    });

                    const res = await request(server, 'DELETE', '/api/ivanti/todo-queue/completed');

                    // Assert preservation behavior
                    expect(res.statusCode).toBe(200);
                    expect(res.body.message).toBe('Completed items cleared.');
                    expect(res.body.deleted).toBe(completedCount);
                }
            ),
            { numRuns: 30, verbose: 2 }
        );
    });

    /**
     * Property 2b: When no completed items exist, returns deleted: 0
     *
     * When the user has no completed items, the SELECT returns empty rows
     * and the endpoint returns { message: 'Completed items cleared.', deleted: 0 }.
     *
     * **Validates: Requirements 3.2**
     */
    it('Property 2b: no completed items returns deleted: 0', async () => {
        await fc.assert(
            fc.asyncProperty(
                fc.constant(null),
                fc.context(),
                async (_unused, ctx) => {
                    ctx.log('Testing with 0 completed items');

                    // Reset mocks for this iteration
                    mockClient.query.mockReset();
                    mockClient.release.mockReset();
                    mockPool.connect.mockReset();
                    mockPool.connect.mockResolvedValue(mockClient);

                    // Mock client.query: SELECT returns empty rows (no completed items)
                    mockClient.query.mockImplementation((text, params) => {
                        if (text === 'BEGIN') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text.includes('SELECT') && text.includes('ivanti_todo_queue') && text.includes('complete')) {
                            return Promise.resolve({ rows: [], rowCount: 0 });
                        }
                        if (text === 'COMMIT') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text === 'ROLLBACK') return Promise.resolve({ rows: [], rowCount: 0 });
                        return Promise.resolve({ rows: [], rowCount: 0 });
                    });

                    const res = await request(server, 'DELETE', '/api/ivanti/todo-queue/completed');

                    expect(res.statusCode).toBe(200);
                    expect(res.body.message).toBe('Completed items cleared.');
                    expect(res.body.deleted).toBe(0);
                }
            ),
            { numRuns: 5, verbose: 2 }
        );
    });

    /**
     * Property 2c: Pending/in-progress items are never touched
     *
     * The SELECT query only fetches items with status = 'complete' for the user.
     * We verify the query text and parameters to ensure non-complete items
     * are never affected.
     *
     * **Validates: Requirements 3.3**
     */
    it('Property 2c: DELETE only targets complete status for the authenticated user', async () => {
        await fc.assert(
            fc.asyncProperty(
                fc.integer({ min: 0, max: 15 }),
                fc.context(),
                async (completedCount, ctx) => {
                    ctx.log(`Testing query targeting with ${completedCount} completed items`);

                    // Reset mocks for this iteration
                    mockClient.query.mockReset();
                    mockClient.release.mockReset();
                    mockPool.connect.mockReset();
                    mockPool.connect.mockResolvedValue(mockClient);

                    // Track the queries issued via client
                    const queriesIssued = [];
                    const itemIds = Array.from({ length: completedCount }, (_, i) => i + 1);

                    mockClient.query.mockImplementation((text, params) => {
                        queriesIssued.push({ text, params });
                        if (text === 'BEGIN') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text.includes('SELECT') && text.includes('ivanti_todo_queue') && text.includes('complete')) {
                            return Promise.resolve({ rows: itemIds.map(id => ({ id })), rowCount: completedCount });
                        }
                        if (text.includes('DELETE FROM jira_ticket_queue_items')) {
                            return Promise.resolve({ rows: [], rowCount: 0 });
                        }
                        if (text.includes('DELETE FROM ivanti_todo_queue')) {
                            return Promise.resolve({ rows: [], rowCount: completedCount });
                        }
                        if (text === 'COMMIT') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text === 'ROLLBACK') return Promise.resolve({ rows: [], rowCount: 0 });
                        return Promise.resolve({ rows: [], rowCount: 0 });
                    });

                    const res = await request(server, 'DELETE', '/api/ivanti/todo-queue/completed');

                    expect(res.statusCode).toBe(200);

                    // Verify the SELECT query targets only complete status for user 42
                    const selectQueries = queriesIssued.filter(q => q.text.includes('SELECT') && q.text.includes('complete'));
                    expect(selectQueries.length).toBeGreaterThanOrEqual(1);

                    for (const q of selectQueries) {
                        expect(q.text).toMatch(/status\s*=\s*'\s*complete\s*'/i);
                        expect(q.text).toMatch(/user_id\s*=/i);
                        expect(q.params).toContain(42);
                    }
                }
            ),
            { numRuns: 20, verbose: 2 }
        );
    });

    /**
     * Property 2d: Other users' items remain untouched
     *
     * The SELECT query is parameterized with req.user.id (mocked as 42).
     * For any random count of completed items, the query only affects user 42's items.
     * We verify the user_id parameter is always the authenticated user's ID.
     *
     * **Validates: Requirements 3.1, 3.3**
     */
    it('Property 2d: DELETE is scoped to the authenticated user only', async () => {
        await fc.assert(
            fc.asyncProperty(
                fc.integer({ min: 1, max: 10 }),
                fc.context(),
                async (completedCount, ctx) => {
                    ctx.log(`Testing user isolation with ${completedCount} completed items`);

                    // Reset mocks for this iteration
                    mockClient.query.mockReset();
                    mockClient.release.mockReset();
                    mockPool.connect.mockReset();
                    mockPool.connect.mockResolvedValue(mockClient);

                    const queriesIssued = [];
                    const itemIds = Array.from({ length: completedCount }, (_, i) => i + 1);

                    mockClient.query.mockImplementation((text, params) => {
                        queriesIssued.push({ text, params });
                        if (text === 'BEGIN') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text.includes('SELECT') && text.includes('ivanti_todo_queue') && text.includes('complete')) {
                            return Promise.resolve({ rows: itemIds.map(id => ({ id })), rowCount: completedCount });
                        }
                        if (text.includes('DELETE FROM jira_ticket_queue_items')) {
                            return Promise.resolve({ rows: [], rowCount: 0 });
                        }
                        if (text.includes('DELETE FROM ivanti_todo_queue')) {
                            return Promise.resolve({ rows: [], rowCount: completedCount });
                        }
                        if (text === 'COMMIT') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text === 'ROLLBACK') return Promise.resolve({ rows: [], rowCount: 0 });
                        return Promise.resolve({ rows: [], rowCount: 0 });
                    });

                    const res = await request(server, 'DELETE', '/api/ivanti/todo-queue/completed');

                    expect(res.statusCode).toBe(200);
                    expect(res.body.deleted).toBe(completedCount);

                    // Verify the SELECT query is scoped to user 42
                    const selectQueries = queriesIssued.filter(q => q.text.includes('SELECT') && q.text.includes('user_id'));
                    for (const q of selectQueries) {
                        expect(q.params).toContain(42); // req.user.id from mock
                    }
                }
            ),
            { numRuns: 20, verbose: 2 }
        );
    });

    /**
     * Property 2e: Response shape is always { message: string, deleted: number }
     *
     * For any random count of completed items (including 0), the response
     * always has the correct shape with message as a string and deleted as a number.
     *
     * **Validates: Requirements 3.1, 3.2**
     */
    it('Property 2e: response shape is { message: string, deleted: number }', async () => {
        await fc.assert(
            fc.asyncProperty(
                fc.integer({ min: 0, max: 50 }),
                fc.context(),
                async (completedCount, ctx) => {
                    ctx.log(`Testing response shape with deleted count: ${completedCount}`);

                    // Reset mocks for this iteration
                    mockClient.query.mockReset();
                    mockClient.release.mockReset();
                    mockPool.connect.mockReset();
                    mockPool.connect.mockResolvedValue(mockClient);

                    const itemIds = Array.from({ length: completedCount }, (_, i) => i + 1);

                    mockClient.query.mockImplementation((text, params) => {
                        if (text === 'BEGIN') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text.includes('SELECT') && text.includes('ivanti_todo_queue') && text.includes('complete')) {
                            return Promise.resolve({ rows: itemIds.map(id => ({ id })), rowCount: completedCount });
                        }
                        if (text.includes('DELETE FROM jira_ticket_queue_items')) {
                            return Promise.resolve({ rows: [], rowCount: 0 });
                        }
                        if (text.includes('DELETE FROM ivanti_todo_queue')) {
                            return Promise.resolve({ rows: [], rowCount: completedCount });
                        }
                        if (text === 'COMMIT') return Promise.resolve({ rows: [], rowCount: 0 });
                        if (text === 'ROLLBACK') return Promise.resolve({ rows: [], rowCount: 0 });
                        return Promise.resolve({ rows: [], rowCount: 0 });
                    });

                    const res = await request(server, 'DELETE', '/api/ivanti/todo-queue/completed');

                    expect(res.statusCode).toBe(200);
                    expect(typeof res.body.message).toBe('string');
                    expect(typeof res.body.deleted).toBe('number');
                    expect(res.body.message).toBe('Completed items cleared.');
                    expect(res.body.deleted).toBe(completedCount);
                }
            ),
            { numRuns: 25, verbose: 2 }
        );
    });
});
