/**
 * Unit and Integration Tests: VCL Compliance Reporting
 *
 * Feature: vcl-compliance-reporting
 *
 * Tests cover:
 * - PATCH /items/:hostname/metadata (happy path, invalid date, plan too long, not found)
 * - GET /vcl/stats with no data (zero/empty response)
 * - Bulk preview with all unmatched hostnames
 * - Bulk preview with mixed valid/invalid rows
 * - Integration test for full bulk flow (preview → commit)
 * - Trend endpoint with < 2 months (no forecast)
 */

const http = require('http');
const express = require('express');

// Mock auth middleware to bypass real session checks
jest.mock('../middleware/auth', () => ({
    requireAuth: () => (req, res, next) => {
        req.user = { id: 1, username: 'testuser', group: 'Admin' };
        next();
    },
    requireGroup: () => (req, res, next) => next(),
}));

// Mock audit log as a no-op
jest.mock('../helpers/auditLog', () => jest.fn());

// Mock ivantiApi to avoid real network calls
jest.mock('../helpers/ivantiApi', () => ({
    ivantiFormPost: jest.fn(),
    ivantiPost: jest.fn(),
}));

// Mock the db pool
const mockPool = {
    query: jest.fn(() => Promise.resolve({ rows: [], rowCount: 0 })),
    connect: jest.fn(() => Promise.resolve({
        query: jest.fn(() => Promise.resolve({ rows: [], rowCount: 0 })),
        release: jest.fn(),
    })),
};
jest.mock('../db', () => mockPool);

// Mock driftChecker to avoid file system dependencies
jest.mock('../helpers/driftChecker', () => ({
    loadConfig: jest.fn(() => ({})),
    compareSchemaToDrift: jest.fn(() => null),
    reconcileConfig: jest.fn(() => ({ changes: [] })),
}));

const { createComplianceRouter } = require('../routes/compliance');

// --- HTTP helper ---

function request(server, method, path, body) {
    return new Promise((resolve, reject) => {
        const addr = server.address();
        const options = {
            hostname: '127.0.0.1',
            port: addr.port,
            path,
            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 rawBody = Buffer.concat(chunks).toString();
                let json;
                try { json = JSON.parse(rawBody); } catch (e) { json = null; }
                resolve({ statusCode: res.statusCode, body: json });
            });
        });

        req.on('error', reject);
        if (body) req.write(JSON.stringify(body));
        req.end();
    });
}

// --- Setup ---

let app, server;

beforeAll((done) => {
    app = express();
    app.use(express.json());

    // Mock multer upload middleware
    const mockUpload = { single: () => (req, res, next) => next() };
    const router = createComplianceRouter(mockUpload);
    app.use('/api/compliance', router);

    server = app.listen(0, '127.0.0.1', done);
});

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

beforeEach(() => {
    mockPool.query.mockReset();
    mockPool.connect.mockReset();
    mockPool.query.mockResolvedValue({ rows: [], rowCount: 0 });
});

// --- 18.1: PATCH /items/:hostname/metadata ---

describe('PATCH /items/:hostname/metadata', () => {
    it('happy path — updates resolution_date and remediation_plan', async () => {
        // Mock client.query: first call = SELECT current values, second+ = INSERT history / UPDATE
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [{ resolution_date: null, remediation_plan: null }], rowCount: 1 }) // SELECT current
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // BEGIN
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (resolution_date)
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (remediation_plan)
                .mockResolvedValueOnce({ rows: [], rowCount: 2 }) // UPDATE items
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // COMMIT
            release: jest.fn(),
        };
        // Override connect to return our mock client
        mockPool.connect.mockResolvedValueOnce(mockClient);
        // The first call from the handler is BEGIN, then SELECT, then inserts, then UPDATE, then COMMIT
        mockClient.query = jest.fn()
            .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
            .mockResolvedValueOnce({ rows: [{ resolution_date: null, remediation_plan: null }], rowCount: 1 }) // SELECT current values
            .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (resolution_date)
            .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (remediation_plan)
            .mockResolvedValueOnce({ rows: [], rowCount: 2 }) // UPDATE items
            .mockResolvedValueOnce({ rows: [], rowCount: 0 }); // COMMIT

        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            remediation_plan: 'Patch in next maintenance window',
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.updated).toBe(2);
    });

    it('returns 400 for invalid date format', async () => {
        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: 'not-a-date',
        });

        expect(res.statusCode).toBe(400);
        expect(res.body.error).toContain('Invalid resolution_date format');
    });

    it('returns 400 when remediation plan exceeds 2000 characters', async () => {
        const longPlan = 'x'.repeat(2001);
        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            remediation_plan: longPlan,
        });

        expect(res.statusCode).toBe(400);
        expect(res.body.error).toContain('2000 characters');
    });

    it('returns 404 when hostname not found', async () => {
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // SELECT current values — empty = not found
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // ROLLBACK
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

        const res = await request(server, 'PATCH', '/api/compliance/items/nonexistent-host/metadata', {
            resolution_date: '2026-06-15',
        });

        expect(res.statusCode).toBe(404);
        expect(res.body.error).toBe('Device not found');
    });
});

// --- 18.2: GET /vcl/stats with no data ---

describe('GET /vcl/stats with no data', () => {
    it('returns zero/empty response when no compliance data exists', async () => {
        // First query: active items
        mockPool.query.mockResolvedValueOnce({ rows: [] });
        // Second query: latest upload
        mockPool.query.mockResolvedValueOnce({ rows: [] });

        const res = await request(server, 'GET', '/api/compliance/vcl/stats');

        expect(res.statusCode).toBe(200);
        expect(res.body.stats).toBeDefined();
        expect(res.body.stats.total_devices).toBe(0);
        expect(res.body.stats.in_scope).toBe(0);
        expect(res.body.stats.compliant).toBe(0);
        expect(res.body.stats.non_compliant).toBe(0);
        expect(res.body.stats.compliance_pct).toBe(0);
        expect(res.body.donut).toBeDefined();
        expect(res.body.heavy_hitters).toEqual([]);
        expect(res.body.vertical_breakdown).toEqual([]);
    });
});

// --- 18.3: Bulk preview with all unmatched hostnames ---

describe('POST /vcl/bulk-preview — all unmatched', () => {
    it('returns all rows as unmatched when no hostnames exist in DB', async () => {
        // Query for existing hostnames returns empty
        mockPool.query.mockResolvedValueOnce({ rows: [] });

        const res = await request(server, 'POST', '/api/compliance/vcl/bulk-preview', {
            rows: [
                { hostname: 'unknown-1', resolution_date: '2026-06-15' },
                { hostname: 'unknown-2', resolution_date: '2026-07-01' },
                { hostname: 'unknown-3', resolution_date: '2026-08-01' },
            ],
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.matched).toBe(0);
        expect(res.body.unmatched).toBe(3);
        expect(res.body.changes).toBe(0);
        expect(res.body.unmatched_rows).toEqual(['unknown-1', 'unknown-2', 'unknown-3']);
    });
});

// --- 18.4: Bulk preview with mixed valid/invalid rows ---

describe('POST /vcl/bulk-preview — mixed valid/invalid', () => {
    it('correctly classifies valid and invalid rows', async () => {
        // Query for existing hostnames
        mockPool.query
            .mockResolvedValueOnce({
                rows: [
                    { hostname: 'srv-001' },
                    { hostname: 'srv-002' },
                    { hostname: 'srv-003' },
                ],
            })
            // Query for current data (DISTINCT ON)
            .mockResolvedValueOnce({
                rows: [
                    { hostname: 'srv-001', resolution_date: null, remediation_plan: null },
                    { hostname: 'srv-003', resolution_date: null, remediation_plan: null },
                ],
            });

        const res = await request(server, 'POST', '/api/compliance/vcl/bulk-preview', {
            rows: [
                { hostname: 'srv-001', resolution_date: '2026-06-15' },       // valid, matched
                { hostname: 'srv-002', resolution_date: 'bad-date' },          // invalid date, matched
                { hostname: 'srv-003', resolution_date: '2026-07-01' },       // valid, matched
                { hostname: 'unknown-1', resolution_date: '2026-08-01' },     // unmatched
            ],
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.matched).toBe(3);
        expect(res.body.unmatched).toBe(1);
        expect(res.body.invalid).toBe(1);
        expect(res.body.invalid_rows[0].hostname).toBe('srv-002');
        expect(res.body.invalid_rows[0].errors[0]).toContain('invalid date');
        expect(res.body.unmatched_rows).toEqual(['unknown-1']);
    });
});

// --- 18.5: Integration test for full bulk flow ---

describe('Integration: full bulk upload flow (preview → commit)', () => {
    it('preview shows changes, commit updates DB', async () => {
        // --- Preview phase ---
        // Query for existing hostnames
        mockPool.query
            .mockResolvedValueOnce({
                rows: [{ hostname: 'srv-001' }, { hostname: 'srv-002' }],
            })
            // Query for current data
            .mockResolvedValueOnce({
                rows: [
                    { hostname: 'srv-001', resolution_date: null, remediation_plan: null },
                    { hostname: 'srv-002', resolution_date: '2026-01-01', remediation_plan: 'Old plan' },
                ],
            });

        const previewRes = await request(server, 'POST', '/api/compliance/vcl/bulk-preview', {
            rows: [
                { hostname: 'srv-001', resolution_date: '2026-06-15', remediation_plan: 'New plan' },
                { hostname: 'srv-002', resolution_date: '2026-01-01', remediation_plan: 'Old plan' }, // unchanged
            ],
        });

        expect(previewRes.statusCode).toBe(200);
        expect(previewRes.body.matched).toBe(2);
        expect(previewRes.body.changes).toBe(1); // only srv-001 changed

        // --- Commit phase ---
        const mockClient = {
            query: jest.fn(),
            release: jest.fn(),
        };
        mockClient.query
            .mockResolvedValueOnce({}) // BEGIN
            .mockResolvedValueOnce({ rows: [{ hostname: 'srv-001', resolution_date: null, remediation_plan: null }] }) // SELECT current values for all hostnames
            .mockResolvedValueOnce({ rowCount: 1 }) // INSERT history (resolution_date)
            .mockResolvedValueOnce({ rowCount: 1 }) // INSERT history (remediation_plan)
            .mockResolvedValueOnce({ rowCount: 1 }) // UPDATE srv-001
            .mockResolvedValueOnce({}); // COMMIT

        mockPool.connect.mockResolvedValueOnce(mockClient);

        const commitRes = await request(server, 'POST', '/api/compliance/vcl/bulk-commit', {
            changes: [
                { hostname: 'srv-001', resolution_date: '2026-06-15', remediation_plan: 'New plan' },
            ],
        });

        expect(commitRes.statusCode).toBe(200);
        expect(commitRes.body.committed).toBe(1);
        expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
        expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
    });
});

// --- 18.6: Trend endpoint with < 2 months (no forecast) ---

describe('GET /vcl/trend — fewer than 2 months', () => {
    it('returns data without forecast when < 2 months exist', async () => {
        mockPool.query.mockResolvedValueOnce({
            rows: [
                { snapshot_month: '2026-01', compliant_count: 900, compliance_pct: '82.0' },
            ],
        });

        const res = await request(server, 'GET', '/api/compliance/vcl/trend');

        expect(res.statusCode).toBe(200);
        expect(res.body.months).toHaveLength(1);
        expect(res.body.months[0].month).toBe('2026-01');
        expect(res.body.months[0].forecast_pct).toBeNull();
        expect(res.body.months[0].target_pct).toBe(95);
    });
});
