/**
 * Unit tests for DELETE /api/ivanti/todo-queue/completed transaction logic
 *
 * Spec: .kiro/specs/ivanti-queue-clear-completed-fix/ (bugfix)
 *
 * Validates: Requirements 2.1, 2.2, 3.1, 3.2
 *
 * Tests verify:
 * - Correct query sequence within a transaction (BEGIN → SELECT → DELETE junction → DELETE queue → COMMIT)
 * - ROLLBACK is called when any query in the transaction fails
 * - Client is always released in the finally block (even on error)
 * - Empty completed set triggers early COMMIT and returns { deleted: 0 }
 * - Response shape is { message: 'Completed items cleared.', deleted: N }
 */
const http = require('http');
const express = require('express');

// --- Mocks ---

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

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

const mockClient = {
    query: jest.fn(),
    release: jest.fn(),
};

const mockPool = {
    query: jest.fn(),
    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();
    });
}

// --- Test Suite ---

describe('DELETE /api/ivanti/todo-queue/completed — Transaction Logic', () => {
    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.connect.mockReset();
        mockPool.connect.mockResolvedValue(mockClient);
    });

    /**
     * Test 1: Correct query sequence
     * Validates: Requirements 2.1, 2.2
     *
     * Verifies the handler issues queries in the correct order:
     * BEGIN → SELECT IDs → DELETE junction → DELETE queue → COMMIT
     */
    it('executes queries in correct transaction order: BEGIN → SELECT → DELETE junction → DELETE queue → COMMIT', async () => {
        const completedIds = [10, 20, 30];

        mockClient.query
            .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
            .mockResolvedValueOnce({ rows: completedIds.map(id => ({ id })), rowCount: 3 }) // SELECT
            .mockResolvedValueOnce({ rows: [], rowCount: 2 }) // DELETE junction
            .mockResolvedValueOnce({ rows: [], rowCount: 3 }) // DELETE queue
            .mockResolvedValueOnce({ rows: [], rowCount: 0 }); // COMMIT

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

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

        // Verify query sequence
        const calls = mockClient.query.mock.calls;
        expect(calls.length).toBe(5);

        // BEGIN
        expect(calls[0][0]).toBe('BEGIN');

        // SELECT completed IDs
        expect(calls[1][0]).toContain('SELECT');
        expect(calls[1][0]).toContain('ivanti_todo_queue');
        expect(calls[1][0]).toContain('complete');
        expect(calls[1][1]).toEqual([7]); // user id

        // DELETE junction table references
        expect(calls[2][0]).toContain('DELETE FROM jira_ticket_queue_items');
        expect(calls[2][0]).toContain('ANY');
        expect(calls[2][1]).toEqual([completedIds]);

        // DELETE queue items
        expect(calls[3][0]).toContain('DELETE FROM ivanti_todo_queue');
        expect(calls[3][0]).toContain('ANY');
        expect(calls[3][1]).toEqual([completedIds]);

        // COMMIT
        expect(calls[4][0]).toBe('COMMIT');
    });

    /**
     * Test 2: ROLLBACK on error
     * Validates: Requirements 2.1
     *
     * Verifies ROLLBACK is called when any query in the transaction fails.
     */
    describe('ROLLBACK on error', () => {
        it('calls ROLLBACK when SELECT query fails', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockRejectedValueOnce(new Error('SELECT failed')); // SELECT throws

            // After the catch, ROLLBACK is called
            mockClient.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // ROLLBACK

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

            expect(res.statusCode).toBe(500);
            expect(res.body).toEqual({ error: 'Internal server error.' });

            const calls = mockClient.query.mock.calls;
            const rollbackCall = calls.find(c => c[0] === 'ROLLBACK');
            expect(rollbackCall).toBeDefined();
        });

        it('calls ROLLBACK when DELETE junction query fails', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ id: 1 }, { id: 2 }], rowCount: 2 }) // SELECT
                .mockRejectedValueOnce(new Error('DELETE junction failed')); // DELETE junction throws

            mockClient.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // ROLLBACK

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

            expect(res.statusCode).toBe(500);
            expect(res.body).toEqual({ error: 'Internal server error.' });

            const calls = mockClient.query.mock.calls;
            const rollbackCall = calls.find(c => c[0] === 'ROLLBACK');
            expect(rollbackCall).toBeDefined();
        });

        it('calls ROLLBACK when DELETE queue query fails', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ id: 5 }], rowCount: 1 }) // SELECT
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // DELETE junction
                .mockRejectedValueOnce(new Error('DELETE queue failed')); // DELETE queue throws

            mockClient.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // ROLLBACK

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

            expect(res.statusCode).toBe(500);
            expect(res.body).toEqual({ error: 'Internal server error.' });

            const calls = mockClient.query.mock.calls;
            const rollbackCall = calls.find(c => c[0] === 'ROLLBACK');
            expect(rollbackCall).toBeDefined();
        });

        it('calls ROLLBACK when COMMIT fails', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ id: 1 }], rowCount: 1 }) // SELECT
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // DELETE junction
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // DELETE queue
                .mockRejectedValueOnce(new Error('COMMIT failed')); // COMMIT throws

            mockClient.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // ROLLBACK

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

            expect(res.statusCode).toBe(500);
            expect(res.body).toEqual({ error: 'Internal server error.' });

            const calls = mockClient.query.mock.calls;
            const rollbackCall = calls.find(c => c[0] === 'ROLLBACK');
            expect(rollbackCall).toBeDefined();
        });
    });

    /**
     * Test 3: Client always released
     * Validates: Requirements 2.1
     *
     * Verifies client.release() is always called in the finally block,
     * even when an error occurs.
     */
    describe('client always released', () => {
        it('releases client after successful transaction', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ id: 1 }], rowCount: 1 }) // SELECT
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // DELETE junction
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // DELETE queue
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }); // COMMIT

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

            expect(mockClient.release).toHaveBeenCalledTimes(1);
        });

        it('releases client after failed transaction (error in SELECT)', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockRejectedValueOnce(new Error('DB error')); // SELECT throws

            mockClient.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // ROLLBACK

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

            expect(mockClient.release).toHaveBeenCalledTimes(1);
        });

        it('releases client after failed transaction (error in DELETE)', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ id: 1 }, { id: 2 }], rowCount: 2 }) // SELECT
                .mockRejectedValueOnce(new Error('FK violation')); // DELETE junction throws

            mockClient.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // ROLLBACK

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

            expect(mockClient.release).toHaveBeenCalledTimes(1);
        });

        it('releases client when empty completed set triggers early return', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // SELECT (empty)
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }); // COMMIT

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

            expect(mockClient.release).toHaveBeenCalledTimes(1);
        });
    });

    /**
     * Test 4: Empty completed set
     * Validates: Requirements 3.1, 3.2
     *
     * When SELECT returns no completed items, the handler should:
     * - Issue COMMIT (not ROLLBACK)
     * - Return { message: 'Completed items cleared.', deleted: 0 }
     * - NOT issue any DELETE queries
     */
    it('empty completed set triggers early COMMIT and returns { deleted: 0 }', async () => {
        mockClient.query
            .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
            .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // SELECT (empty)
            .mockResolvedValueOnce({ rows: [], rowCount: 0 }); // COMMIT

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

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

        // Verify query sequence: BEGIN → SELECT → COMMIT (no DELETEs)
        const calls = mockClient.query.mock.calls;
        expect(calls.length).toBe(3);
        expect(calls[0][0]).toBe('BEGIN');
        expect(calls[1][0]).toContain('SELECT');
        expect(calls[2][0]).toBe('COMMIT');

        // No DELETE queries issued
        const deleteQueries = calls.filter(c => c[0].includes('DELETE'));
        expect(deleteQueries.length).toBe(0);
    });

    /**
     * Test 5: Response shape preserved
     * Validates: Requirements 2.2, 3.1
     *
     * Verifies the response is always { message: 'Completed items cleared.', deleted: N }
     */
    describe('response shape preserved', () => {
        it('returns correct shape with deleted count matching rowCount', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }], rowCount: 5 }) // SELECT
                .mockResolvedValueOnce({ rows: [], rowCount: 3 }) // DELETE junction
                .mockResolvedValueOnce({ rows: [], rowCount: 5 }) // DELETE queue
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }); // COMMIT

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

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

        it('returns correct shape with single deleted item', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ id: 99 }], rowCount: 1 }) // SELECT
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // DELETE junction
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // DELETE queue
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }); // COMMIT

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

            expect(res.statusCode).toBe(200);
            expect(res.body).toEqual({
                message: 'Completed items cleared.',
                deleted: 1,
            });
        });

        it('returns error shape on failure', async () => {
            mockClient.query
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockRejectedValueOnce(new Error('Something broke')); // SELECT throws

            mockClient.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // ROLLBACK

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

            expect(res.statusCode).toBe(500);
            expect(res.body).toEqual({ error: 'Internal server error.' });
        });
    });
});
