/**
 * Unit Tests: PATCH /api/compliance/items/:hostname/metadata — Per-Metric Scoping
 *
 * Feature: remediation-plan-history (per-metric extension)
 *
 * Tests cover:
 * - Task 8.1: metric_id/metric_ids validation, precedence, non-empty/max 100 chars, active item check
 * - Task 8.2: Per-metric SELECT, INSERT history per metric, UPDATE only matching rows
 * - Task 8.3: Hostname-level behavior preserved with NULL metric_id in history
 *
 * Validates: Requirements 8, 11, 15
 */

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

// Mock auth middleware
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
jest.mock('../helpers/ivantiApi', () => ({
    ivantiFormPost: jest.fn(),
    ivantiPost: jest.fn(),
}));

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

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

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());
    const mockUpload = { single: () => (req, res, next) => next() };
    app.use('/api/compliance', createComplianceRouter(mockUpload));
    server = app.listen(0, '127.0.0.1', done);
});

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

beforeEach(() => {
    jest.clearAllMocks();
});

// --- Task 8.1: Validation ---

describe('Task 8.1: metric_id/metric_ids validation', () => {
    it('returns 400 when metric_ids is not an array', async () => {
        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_ids: 'not-an-array',
        });
        expect(res.statusCode).toBe(400);
        expect(res.body.error).toBe('metric_ids must be an array');
    });

    it('returns 400 when metric_ids is empty array', async () => {
        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_ids: [],
        });
        expect(res.statusCode).toBe(400);
        expect(res.body.error).toBe('metric_ids must contain at least one entry');
    });

    it('returns 400 when metric_ids contains empty string', async () => {
        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_ids: ['2.1.1', ''],
        });
        expect(res.statusCode).toBe(400);
        expect(res.body.error).toBe('metric_id cannot be empty');
    });

    it('returns 400 when metric_id exceeds 100 characters', async () => {
        const longId = 'x'.repeat(101);
        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_ids: [longId],
        });
        expect(res.statusCode).toBe(400);
        expect(res.body.error).toBe('metric_id exceeds 100 characters');
    });

    it('returns 400 when single metric_id is empty string', async () => {
        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_id: '',
        });
        expect(res.statusCode).toBe(400);
        expect(res.body.error).toBe('metric_id cannot be empty');
    });

    it('returns 400 when single metric_id exceeds 100 characters', async () => {
        const longId = 'x'.repeat(101);
        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_id: longId,
        });
        expect(res.statusCode).toBe(400);
        expect(res.body.error).toBe('metric_id exceeds 100 characters');
    });

    it('returns 400 when metric_id does not correspond to active compliance_item', async () => {
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // SELECT active metrics — none found
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // ROLLBACK
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_ids: ['nonexistent-metric'],
        });
        expect(res.statusCode).toBe(400);
        expect(res.body.error).toContain('Invalid metric_id: nonexistent-metric');
    });

    it('uses metric_ids when both metric_id and metric_ids are provided', async () => {
        // metric_ids wins — should validate metric_ids, not metric_id
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ metric_id: '3.1.1', resolution_date: null, remediation_plan: null }], rowCount: 1 }) // SELECT active metrics
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // UPDATE
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // COMMIT
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_id: '2.1.1',       // should be ignored
            metric_ids: ['3.1.1'],    // should be used
        });
        expect(res.statusCode).toBe(200);
        // Verify the SELECT query used metric_ids value ['3.1.1'], not metric_id '2.1.1'
        const selectCall = mockClient.query.mock.calls[1];
        expect(selectCall[1]).toEqual(['srv-001', ['3.1.1']]);
    });
});

// --- Task 8.2: Per-metric scoping behavior ---

describe('Task 8.2: Per-metric SELECT, INSERT history, UPDATE matching rows', () => {
    it('selects current values per targeted metric and inserts history per metric', async () => {
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [
                    { metric_id: '2.1.1', resolution_date: '2026-01-01', remediation_plan: 'Plan A' },
                    { metric_id: '2.3.2', resolution_date: '2026-02-01', remediation_plan: 'Plan B' },
                ], rowCount: 2 }) // SELECT active metrics with current values
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history for 2.1.1 resolution_date
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history for 2.1.1 remediation_plan
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history for 2.3.2 resolution_date
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history for 2.3.2 remediation_plan
                .mockResolvedValueOnce({ rows: [], rowCount: 2 }) // UPDATE matching rows
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // COMMIT
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            remediation_plan: 'New unified plan',
            metric_ids: ['2.1.1', '2.3.2'],
        });

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

        // Verify history inserts include metric_id
        const calls = mockClient.query.mock.calls;
        // Call [2] = INSERT history for 2.1.1 resolution_date
        expect(calls[2][0]).toContain('INSERT INTO compliance_item_history');
        expect(calls[2][1][1]).toBe('2.1.1'); // metric_id
        expect(calls[2][1][2]).toBe('2026-01-01'); // old_value
        expect(calls[2][1][3]).toBe('2026-06-15'); // new_value

        // Call [3] = INSERT history for 2.1.1 remediation_plan
        expect(calls[3][1][1]).toBe('2.1.1');
        expect(calls[3][1][2]).toBe('Plan A'); // old_value
        expect(calls[3][1][3]).toBe('New unified plan'); // new_value

        // Call [4] = INSERT history for 2.3.2 resolution_date
        expect(calls[4][1][1]).toBe('2.3.2');
        expect(calls[4][1][2]).toBe('2026-02-01'); // old_value

        // Call [5] = INSERT history for 2.3.2 remediation_plan
        expect(calls[5][1][1]).toBe('2.3.2');
        expect(calls[5][1][2]).toBe('Plan B'); // old_value
    });

    it('skips history insert when value is unchanged for a specific metric', async () => {
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [
                    { metric_id: '2.1.1', resolution_date: '2026-06-15', remediation_plan: null },
                ], rowCount: 1 }) // SELECT — already has the target date
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // UPDATE (no history inserts since value unchanged)
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // COMMIT
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

        const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
            resolution_date: '2026-06-15',
            metric_ids: ['2.1.1'],
        });

        expect(res.statusCode).toBe(200);
        // No INSERT history calls — only BEGIN, SELECT, UPDATE, COMMIT
        const calls = mockClient.query.mock.calls;
        expect(calls.length).toBe(4);
        expect(calls[2][0]).toContain('UPDATE compliance_items');
    });

    it('updates only matching rows with metric_id = ANY filter', async () => {
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [
                    { metric_id: '2.1.1', resolution_date: null, remediation_plan: null },
                ], rowCount: 1 }) // SELECT
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // UPDATE
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // COMMIT
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

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

        expect(res.statusCode).toBe(200);
        // Verify UPDATE query includes metric_id = ANY filter
        const updateCall = mockClient.query.mock.calls[3];
        expect(updateCall[0]).toContain('metric_id = ANY');
        expect(updateCall[0]).toContain("status = 'active'");
        expect(updateCall[1]).toContain('srv-001');
        expect(updateCall[1]).toEqual(expect.arrayContaining([['2.1.1']]));
    });
});

// --- Task 8.3: Hostname-level behavior preserved ---

describe('Task 8.3: Hostname-level behavior with NULL metric_id', () => {
    it('updates all active rows when no metric_id/metric_ids provided', async () => {
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ resolution_date: null, remediation_plan: null }], rowCount: 1 }) // SELECT current
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history
                .mockResolvedValueOnce({ rows: [], rowCount: 5 }) // UPDATE all active rows
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // COMMIT
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

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

        expect(res.statusCode).toBe(200);
        expect(res.body.updated).toBe(5);

        // Verify UPDATE does NOT include metric_id filter
        const updateCall = mockClient.query.mock.calls[3];
        expect(updateCall[0]).not.toContain('metric_id');
        expect(updateCall[0]).toContain("status = 'active'");
    });

    it('inserts history with NULL metric_id when no metric scoping', async () => {
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [{ resolution_date: '2026-01-01', remediation_plan: 'Old plan' }], rowCount: 1 }) // SELECT current
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (resolution_date)
                .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (remediation_plan)
                .mockResolvedValueOnce({ rows: [], rowCount: 3 }) // UPDATE
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // COMMIT
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

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

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

        // Verify history INSERT includes NULL for metric_id
        const historyCall1 = mockClient.query.mock.calls[2];
        expect(historyCall1[0]).toContain('INSERT INTO compliance_item_history');
        expect(historyCall1[0]).toContain('NULL');
    });

    it('returns 404 when hostname has no active items (hostname-level path)', async () => {
        const mockClient = {
            query: jest.fn()
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // SELECT current — empty
                .mockResolvedValueOnce({ rows: [], rowCount: 0 }), // ROLLBACK
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(mockClient);

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

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