/**
 * Bug Condition Exploration Property Tests: Compliance Duplicate Chart Entries
 *
 * Spec: .kiro/specs/compliance-duplicate-chart-entries/ (bugfix)
 *
 * BUG CONDITION (from design.md):
 *   EXISTS report_date d WHERE COUNT(compliance_uploads WHERE report_date = d) > 1
 *
 * Five compliance code paths share the root cause "key by `compliance_uploads.id`
 * instead of by `compliance_uploads.report_date`":
 *   - GET /trends                  (Property 1, test case 1.A)
 *   - GET /top-recurring           (Property 2, test case 1.B)
 *   - GET /category-trend          (Property 3, test case 1.C)
 *   - GET /summary                 (Property 4, test case 1.D)
 *   - persistUpload() snapshots    (Property 5, test case 1.E)
 *
 * THIS TEST SUITE IS EXPECTED TO FAIL ON UNFIXED CODE.
 * Failure of these five test cases is the SUCCESS CASE for the exploration —
 * each failure is a counterexample that confirms the corresponding manifestation
 * of the bug exists. After the five fixes from design.md are implemented,
 * these same cases will pass and become regression guards.
 *
 * Each case is anchored on the canonical fixture
 * (`fixture_multi_vertical_single_date` from design.md) AND wrapped in a
 * fast-check `fc.assert` against `arbScenario` so the property is also
 * exercised on randomly-generated multi-vertical scenarios with colliding
 * `report_date`s.
 *
 * **Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 1.11**
 */

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: 1, username: 'testuser', group: 'Admin' };
        next();
    },
    requireGroup: () => (req, res, next) => next(),
}));

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

// Programmable pg pool: each test installs a query handler that matches the
// actual SQL fragments emitted by backend/routes/compliance.js. The default
// handler returns an empty rowset.
let queryHandler = () => Promise.resolve({ rows: [], rowCount: 0 });
const recordedQueries = [];

const mockPool = {
    query: jest.fn((text, params) => {
        recordedQueries.push({ text, params, on: 'pool' });
        return queryHandler(text, params);
    }),
    connect: jest.fn(() => Promise.resolve({
        query: jest.fn((text, params) => {
            recordedQueries.push({ text, params, on: 'client' });
            return queryHandler(text, params);
        }),
        release: jest.fn(),
    })),
};
jest.mock('../db', () => mockPool);

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

// --- HTTP helper ---

function request(server, method, urlPath, body) {
    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 json;
                try { json = JSON.parse(raw); } catch { json = null; }
                resolve({ statusCode: res.statusCode, body: json });
            });
        });
        req.on('error', reject);
        if (body !== undefined) req.write(JSON.stringify(body));
        req.end();
    });
}

// --- Pool router: dispatch by SQL substring/regex ---

/**
 * Build a query handler from an ordered list of routes. Each route's `match`
 * is a substring or RegExp tested against the SQL text. The first match wins.
 * `rows` may be a static array or a function (text, params) => rows.
 */
function makeQueryHandler(routes) {
    return (text, params) => {
        for (const route of routes) {
            const target = route.match;
            const hit = target instanceof RegExp ? target.test(text) : text.includes(target);
            if (hit) {
                const rows = typeof route.rows === 'function'
                    ? (route.rows(text, params) || [])
                    : (route.rows || []);
                return Promise.resolve({ rows, rowCount: rows.length });
            }
        }
        return Promise.resolve({ rows: [], rowCount: 0 });
    };
}

// --- Fixture builders (per design.md "Test Fixtures Required") ---

const TEAMS = ['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV'];
const CATEGORIES = ['Patching', 'Configuration', 'Vulnerability', 'Other'];

/**
 * fixture_multi_vertical_single_date — three uploads sharing 2025-05-11.
 * Reproduces the original GitLab #12 scenario.
 *
 * Each upload has a distinct vertical (NTS_AEO, SDIT_CISO, TSI), distinct
 * counts, and 6 items spread across two teams (STEAM, ACCESS-ENG) and two
 * categories (Patching, Configuration). Item layout per upload:
 *   STEAM      / Patching      x2
 *   STEAM      / Configuration x1
 *   ACCESS-ENG / Patching      x1
 *   ACCESS-ENG / Configuration x2
 * Per-date aggregate (3 uploads): 9 STEAM, 9 ACCESS-ENG, 9 Patching, 9 Configuration.
 */
function fixtureMultiVerticalSingleDate() {
    const verticals = ['NTS_AEO', 'SDIT_CISO', 'TSI'];
    const uploads = verticals.map((v, idx) => ({
        id: 300 + idx,
        report_date: '2025-05-11',
        vertical: v,
        new_count: 3 + idx,            // 3, 4, 5    sum = 12
        recurring_count: 7 + idx * 2,  // 7, 9, 11   sum = 27
        resolved_count: 1 + idx,       // 1, 2, 3    sum = 6
        uploaded_at: `2025-05-11T${10 + idx}:00:00Z`,
        summary_json: JSON.stringify({
            entries: [{ team: 'STEAM', metric: 'patching', score: 80 + idx }],
            overall_scores: { patching: 80 + idx },
        }),
    }));

    const items = [];
    let itemId = 2000;
    const layout = [
        { team: 'STEAM',      category: 'Patching' },
        { team: 'STEAM',      category: 'Patching' },
        { team: 'STEAM',      category: 'Configuration' },
        { team: 'ACCESS-ENG', category: 'Patching' },
        { team: 'ACCESS-ENG', category: 'Configuration' },
        { team: 'ACCESS-ENG', category: 'Configuration' },
    ];
    for (const u of uploads) {
        for (const l of layout) {
            items.push({
                id: itemId++,
                upload_id: u.id,
                hostname: `${u.vertical}-host-${itemId}`,
                team: l.team,
                category: l.category,
                vertical: u.vertical,
                status: 'active',
            });
        }
    }
    return { uploads, items };
}

/**
 * fixture_cross_vertical_items — two disjoint sets of compliance_items.
 * NTS_AEO contributes 100 hosts on team STEAM.
 * SDIT_CISO contributes 50 hosts on team ACCESS-ENG.
 * Used to exercise the persistUpload() vertical-isolation property (1.E).
 */
function fixtureCrossVerticalItems() {
    const items = [];
    let id = 5000;
    for (let i = 1; i <= 100; i++) {
        items.push({
            id: id++,
            hostname: `nts-aeo-host-${i}`,
            team: 'STEAM',
            vertical: 'NTS_AEO',
            status: 'active',
        });
    }
    for (let i = 1; i <= 50; i++) {
        items.push({
            id: id++,
            hostname: `sdit-ciso-host-${i}`,
            team: 'ACCESS-ENG',
            vertical: 'SDIT_CISO',
            status: 'active',
        });
    }
    return items;
}

// --- fast-check arbitraries (design.md fixture_pbt_generators, restricted to
//     scenarios where the bug condition holds: at least one report_date has
//     two or more upload rows). ---

const arbReportDate = fc.constantFrom('2025-05-04', '2025-05-11', '2025-05-18');
const arbVertical   = fc.constantFrom('NTS_AEO', 'SDIT_CISO', 'TSI', null);

const arbUpload = fc.record({
    report_date:     arbReportDate,
    vertical:        arbVertical,
    new_count:       fc.integer({ min: 0, max: 30 }),
    recurring_count: fc.integer({ min: 0, max: 30 }),
    resolved_count:  fc.integer({ min: 0, max: 30 }),
});

/**
 * arbScenario — a list of compliance_uploads rows where at least one
 * report_date appears in two or more rows (i.e., the bug condition holds).
 * The pre-condition is enforced post-generation via filter() so fast-check
 * shrinking still finds simple counterexamples.
 */
const arbScenario = fc.array(arbUpload, { minLength: 2, maxLength: 6 })
    .filter(arr => {
        const counts = {};
        for (const u of arr) counts[u.report_date] = (counts[u.report_date] || 0) + 1;
        return Object.values(counts).some(c => c > 1);
    })
    .map((rawUploads) => rawUploads.map((u, i) => ({
        id: 1000 + i,
        uploaded_at: `${u.report_date}T${10 + i}:00:00Z`,
        summary_json: JSON.stringify({
            entries: [{ team: 'STEAM', metric: 'patching', score: 80 }],
            overall_scores: { patching: 80 },
        }),
        ...u,
    })));

/**
 * arbScenarioWithItems — arbScenario plus a small set of compliance_items
 * (one to four per upload), used by the /category-trend property test.
 */
const arbScenarioWithItems = arbScenario.chain(uploads => {
    const itemArrays = uploads.map(() => fc.array(
        fc.record({
            team:     fc.constantFrom(...TEAMS),
            category: fc.constantFrom(...CATEGORIES),
        }),
        { minLength: 1, maxLength: 4 },
    ));
    return fc.tuple(fc.constant(uploads), fc.tuple(...itemArrays))
        .map(([ups, perUpload]) => {
            const items = [];
            let itemId = 9000;
            ups.forEach((u, i) => {
                for (const it of perUpload[i]) {
                    items.push({
                        id: itemId++,
                        upload_id: u.id,
                        hostname: `host-${itemId}`,
                        vertical: u.vertical,
                        status: 'active',
                        ...it,
                    });
                }
            });
            return { uploads: ups, items };
        });
});

// --- Shared mock builders for the four read endpoints ---

/**
 * Build a query handler that simulates the database state given a list of
 * uploads and a list of items. The handler responds to BOTH the unfixed and
 * fixed shapes of the SQL — the unfixed shape returns one row per upload,
 * the fixed shape groups by report_date — so the same test code is
 * meaningful when re-run against fixed code in tasks 3.2 / 4.2 / 5.2.
 *
 * The actual SQL fragments are taken verbatim from backend/routes/compliance.js.
 */
function installReadEndpointHandler(uploads, items) {
    queryHandler = makeQueryHandler([
        // ----- /trends and /top-recurring primary uploads listing -----
        // Unfixed: SELECT id, report_date, ..., COALESCE(...) AS total_active FROM compliance_uploads ORDER BY report_date ASC
        // Fixed:   SELECT report_date, SUM(...) ... GROUP BY report_date ORDER BY report_date ASC
        {
            match: /FROM\s+compliance_uploads(?!\s+cu\b)[\s\S]*ORDER\s+BY\s+report_date\s+ASC/i,
            rows: (text) => {
                if (/GROUP\s+BY\s+report_date/i.test(text)) {
                    // FIXED shape: aggregate by report_date
                    const byDate = {};
                    for (const u of uploads) {
                        const d = u.report_date;
                        if (!byDate[d]) {
                            byDate[d] = {
                                report_date: d,
                                new_count: 0,
                                recurring_count: 0,
                                resolved_count: 0,
                                total_active: 0,
                            };
                        }
                        byDate[d].new_count       += u.new_count;
                        byDate[d].recurring_count += u.recurring_count;
                        byDate[d].resolved_count  += u.resolved_count;
                        byDate[d].total_active    += u.new_count + u.recurring_count;
                    }
                    return Object.values(byDate).sort((a, b) =>
                        a.report_date.localeCompare(b.report_date),
                    );
                }
                // UNFIXED shape: one row per upload
                return [...uploads]
                    .sort((a, b) => a.report_date.localeCompare(b.report_date))
                    .map(u => ({
                        id: u.id,
                        report_date: u.report_date,
                        new_count: u.new_count,
                        recurring_count: u.recurring_count,
                        resolved_count: u.resolved_count,
                        total_active: u.new_count + u.recurring_count,
                    }));
            },
        },

        // ----- /trends per-team item-count query -----
        // Unfixed: SELECT ci.upload_id, ci.team, COUNT(ci.id)::int FROM compliance_items ci WHERE ci.team IS NOT NULL GROUP BY ci.upload_id, ci.team
        // Fixed:   SELECT cu.report_date, ci.team, COUNT(...)         FROM compliance_items ci JOIN compliance_uploads cu ON ci.upload_id = cu.id WHERE ci.team IS NOT NULL ... GROUP BY cu.report_date, ci.team
        {
            match: /FROM\s+compliance_items\s+ci\b[\s\S]*?WHERE\s+ci\.team\s+IS\s+NOT\s+NULL/i,
            rows: (text) => {
                if (/GROUP\s+BY\s+cu\.report_date/i.test(text)) {
                    // FIXED shape
                    const grouped = {};
                    for (const it of items) {
                        if (!it.team) continue;
                        const u = uploads.find(x => x.id === it.upload_id);
                        if (!u || u.report_date == null) continue;
                        const k = `${u.report_date}|${it.team}`;
                        grouped[k] = (grouped[k] || 0) + 1;
                    }
                    return Object.entries(grouped).map(([k, count]) => {
                        const [report_date, team] = k.split('|');
                        return { report_date, team, count };
                    });
                }
                // UNFIXED shape: keyed by upload_id
                const grouped = {};
                for (const it of items) {
                    if (!it.team) continue;
                    const k = `${it.upload_id}|${it.team}`;
                    grouped[k] = (grouped[k] || 0) + 1;
                }
                return Object.entries(grouped).map(([k, count]) => {
                    const [upload_id, team] = k.split('|');
                    return { upload_id: Number(upload_id), team, count };
                });
            },
        },

        // ----- /category-trend -----
        // Unfixed: SELECT cu.report_date, COALESCE(ci.category, 'Unknown') AS category, COUNT(ci.id)::int AS count
        //          FROM compliance_uploads cu JOIN compliance_items ci ON ci.upload_id = cu.id
        //          GROUP BY cu.id, cu.report_date, category ORDER BY cu.report_date ASC
        // Fixed:   ... GROUP BY cu.report_date, COALESCE(ci.category, 'Unknown') ORDER BY ...
        {
            match: /FROM\s+compliance_uploads\s+cu\s+JOIN\s+compliance_items\s+ci/i,
            rows: (text) => {
                if (/GROUP\s+BY\s+cu\.id/i.test(text)) {
                    // UNFIXED shape: group by (upload_id, date, category) → duplicate rows per date
                    const grouped = {};
                    for (const it of items) {
                        const u = uploads.find(x => x.id === it.upload_id);
                        if (!u || u.report_date == null) continue;
                        const cat = it.category || 'Unknown';
                        const k = `${u.id}|${u.report_date}|${cat}`;
                        grouped[k] = (grouped[k] || 0) + 1;
                    }
                    return Object.entries(grouped)
                        .map(([k, count]) => {
                            const [, report_date, category] = k.split('|');
                            return { report_date, category, count };
                        })
                        .sort((a, b) =>
                            a.report_date.localeCompare(b.report_date) ||
                            a.category.localeCompare(b.category),
                        );
                }
                // FIXED shape: group by (date, category)
                const grouped = {};
                for (const it of items) {
                    const u = uploads.find(x => x.id === it.upload_id);
                    if (!u || u.report_date == null) continue;
                    const cat = it.category || 'Unknown';
                    const k = `${u.report_date}|${cat}`;
                    grouped[k] = (grouped[k] || 0) + 1;
                }
                return Object.entries(grouped)
                    .map(([k, count]) => {
                        const [report_date, category] = k.split('|');
                        return { report_date, category, count };
                    })
                    .sort((a, b) =>
                        a.report_date.localeCompare(b.report_date) ||
                        a.category.localeCompare(b.category),
                    );
            },
        },

        // ----- /summary primary upload selection -----
        // Unfixed: WHERE vertical IS NULL ORDER BY id DESC LIMIT 1
        {
            match: /WHERE\s+vertical\s+IS\s+NULL\s+ORDER\s+BY\s+id\s+DESC\s+LIMIT\s+1/i,
            rows: () => {
                const candidates = uploads
                    .filter(u => u.vertical == null && u.summary_json)
                    .sort((a, b) => b.id - a.id);
                if (candidates.length === 0) return [];
                const u = candidates[0];
                return [{
                    id: u.id,
                    summary_json: u.summary_json,
                    report_date: u.report_date,
                    uploaded_at: u.uploaded_at,
                }];
            },
        },
        // Unfixed fallback: WHERE vertical = 'NTS_AEO' ORDER BY id DESC LIMIT 1
        {
            match: /WHERE\s+vertical\s*=\s*'NTS_AEO'\s+ORDER\s+BY\s+id\s+DESC\s+LIMIT\s+1/i,
            rows: () => {
                const candidates = uploads
                    .filter(u => u.vertical === 'NTS_AEO' && u.summary_json)
                    .sort((a, b) => b.id - a.id);
                if (candidates.length === 0) return [];
                const u = candidates[0];
                return [{
                    id: u.id,
                    summary_json: u.summary_json,
                    report_date: u.report_date,
                    uploaded_at: u.uploaded_at,
                }];
            },
        },
        // Fixed-side: sibling-disclosure query
        // SELECT id, vertical, uploaded_at FROM compliance_uploads WHERE report_date = $1 AND id != $2 ORDER BY id ASC
        {
            match: /WHERE\s+report_date\s*=\s*\$1\s+AND\s+id\s*!=\s*\$2/i,
            rows: (_text, params) => {
                const [reportDate, excludeId] = params || [];
                return uploads
                    .filter(u => u.report_date === reportDate && u.id !== excludeId)
                    .sort((a, b) => a.id - b.id)
                    .map(u => ({ id: u.id, vertical: u.vertical, uploaded_at: u.uploaded_at }));
            },
        },
    ]);
}

// --- Express server 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(() => {
    mockPool.query.mockClear();
    mockPool.connect.mockClear();
    recordedQueries.length = 0;
    queryHandler = () => Promise.resolve({ rows: [], rowCount: 0 });
});

// =============================================================================
// Test Case 1.A — Property 1: Bug Condition — `/trends` returns one entry
//                              per unique report_date with summed counts and
//                              aggregated per-team totals.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
//   With three compliance_uploads for 2025-05-11 (NTS_AEO, SDIT_CISO, TSI),
//   GET /trends returns three entries with report_date='2025-05-11' instead
//   of one. The handler runs `SELECT id, report_date, ... FROM
//   compliance_uploads ORDER BY report_date ASC` and `.map()`s each row into
//   a trend entry; per-team counts are pre-aggregated by upload_id and
//   looked up by `u.id`, so duplicate-date rows produce duplicate-date
//   trend entries with split per-team counts.
//
// **Validates: Requirements 1.1, 1.2, 1.3**
//
describe('Bug Condition / Property 1 — GET /trends aggregates uploads by report_date', () => {
    it('1.A canonical fixture — exactly one entry per unique report_date with summed counts and aggregated per-team totals', async () => {
        const { uploads, items } = fixtureMultiVerticalSingleDate();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/trends');
        expect(res.statusCode).toBe(200);

        // (1) Exactly one entry per unique report_date
        const targetDate = '2025-05-11';
        const matchingTrends = res.body.trends.filter(t => t.report_date === targetDate);
        expect(matchingTrends).toHaveLength(1);

        // (2) Counts equal SUM across all uploads sharing that date
        const targetUploads = uploads.filter(u => u.report_date === targetDate);
        const expectedNew       = targetUploads.reduce((s, u) => s + u.new_count, 0);
        const expectedRecurring = targetUploads.reduce((s, u) => s + u.recurring_count, 0);
        const expectedResolved  = targetUploads.reduce((s, u) => s + u.resolved_count, 0);
        const expectedTotalActive = targetUploads.reduce((s, u) => s + u.new_count + u.recurring_count, 0);

        const trend = matchingTrends[0];
        expect(trend.new_count).toBe(expectedNew);
        expect(trend.recurring_count).toBe(expectedRecurring);
        expect(trend.resolved_count).toBe(expectedResolved);
        expect(trend.total_active).toBe(expectedTotalActive);

        // (3) Per-team counts equal SUM across uploads sharing that date.
        // Layout per upload: 3 STEAM, 3 ACCESS-ENG. Three uploads share the
        // date → 9 STEAM, 9 ACCESS-ENG, 0 ACCESS-OPS, 0 INTELDEV.
        const targetItems = items.filter(it =>
            targetUploads.some(u => u.id === it.upload_id),
        );
        const expectedSTEAM      = targetItems.filter(it => it.team === 'STEAM').length;
        const expectedAccessEng  = targetItems.filter(it => it.team === 'ACCESS-ENG').length;
        const expectedAccessOps  = targetItems.filter(it => it.team === 'ACCESS-OPS').length;
        const expectedIntelDev   = targetItems.filter(it => it.team === 'INTELDEV').length;

        expect(trend.STEAM).toBe(expectedSTEAM);
        expect(trend['ACCESS-ENG']).toBe(expectedAccessEng);
        expect(trend['ACCESS-OPS']).toBe(expectedAccessOps);
        expect(trend.INTELDEV).toBe(expectedIntelDev);
    });

    it('1.A property — GET /trends returns one entry per unique report_date for any multi-vertical scenario', async () => {
        await fc.assert(
            fc.asyncProperty(arbScenario, async (uploads) => {
                installReadEndpointHandler(uploads, []);
                const res = await request(server, 'GET', '/api/compliance/trends');
                expect(res.statusCode).toBe(200);

                const uniqueDates = new Set(uploads.map(u => u.report_date));
                expect(res.body.trends).toHaveLength(uniqueDates.size);

                // For each unique date, the entry's counts are the SUM
                // across all uploads sharing that date.
                for (const date of uniqueDates) {
                    const sharing = uploads.filter(u => u.report_date === date);
                    const entry = res.body.trends.find(t => t.report_date === date);
                    expect(entry).toBeDefined();
                    expect(entry.new_count).toBe(sharing.reduce((s, u) => s + u.new_count, 0));
                    expect(entry.recurring_count).toBe(sharing.reduce((s, u) => s + u.recurring_count, 0));
                    expect(entry.resolved_count).toBe(sharing.reduce((s, u) => s + u.resolved_count, 0));
                }
            }),
            { numRuns: 25 },
        );
    });
});

// =============================================================================
// Test Case 1.B — Property 2: Bug Condition — `/top-recurring` waterfall has
//                              one bar per unique report_date with correct
//                              running totals.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
//   Three uploads for 2025-05-11 produce three waterfall bars labelled
//   "2025-05-11". Worse, computeWaterfall() carries `start` forward across
//   the three rows, so the second and third bars' start reflects the prior
//   row's end inside the same date — the running totals misrepresent the
//   date-level deltas. The fix aggregates uploads to one row per date
//   before passing to computeWaterfall().
//
// **Validates: Requirements 1.4, 1.5**
//
describe('Bug Condition / Property 2 — GET /top-recurring has one bar per unique report_date with running invariant', () => {
    it('1.B canonical fixture — exactly one waterfall entry per unique report_date and running invariant holds', async () => {
        const { uploads, items } = fixtureMultiVerticalSingleDate();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/top-recurring');
        expect(res.statusCode).toBe(200);

        const wf = res.body.waterfall;
        // (1) One waterfall entry per unique report_date
        const dates = wf.map(w => w.date);
        const uniqueDates = new Set(uploads.map(u => u.report_date));
        expect(wf).toHaveLength(uniqueDates.size);
        expect(new Set(dates).size).toBe(uniqueDates.size);

        // (2) Running invariant: entry[0].start === 0 AND
        //     entry[i].end === entry[i].start + new_count + recurring_count - resolved_count
        //     entry[i].start === entry[i-1].end (for i >= 1)
        expect(wf[0].start).toBe(0);
        for (let i = 0; i < wf.length; i++) {
            expect(wf[i].end).toBe(
                wf[i].start + wf[i].new_count + wf[i].recurring_count - wf[i].resolved_count,
            );
            if (i > 0) {
                expect(wf[i].start).toBe(wf[i - 1].end);
            }
        }

        // (3) For 2025-05-11, the new/recurring/resolved counts equal the
        //     SUM across all uploads sharing that date.
        const target = wf.find(w => w.date === '2025-05-11');
        const sharing = uploads.filter(u => u.report_date === '2025-05-11');
        expect(target.new_count).toBe(sharing.reduce((s, u) => s + u.new_count, 0));
        expect(target.recurring_count).toBe(sharing.reduce((s, u) => s + u.recurring_count, 0));
        expect(target.resolved_count).toBe(sharing.reduce((s, u) => s + u.resolved_count, 0));
    });

    it('1.B property — waterfall has exactly one entry per unique report_date and the running invariant always holds', async () => {
        await fc.assert(
            fc.asyncProperty(arbScenario, async (uploads) => {
                installReadEndpointHandler(uploads, []);
                const res = await request(server, 'GET', '/api/compliance/top-recurring');
                expect(res.statusCode).toBe(200);

                const wf = res.body.waterfall;
                const uniqueDates = new Set(uploads.map(u => u.report_date));
                expect(wf).toHaveLength(uniqueDates.size);

                if (wf.length > 0) {
                    expect(wf[0].start).toBe(0);
                }
                for (let i = 0; i < wf.length; i++) {
                    expect(wf[i].end).toBe(
                        wf[i].start + wf[i].new_count + wf[i].recurring_count - wf[i].resolved_count,
                    );
                    if (i > 0) {
                        expect(wf[i].start).toBe(wf[i - 1].end);
                    }
                }
            }),
            { numRuns: 25 },
        );
    });
});

// =============================================================================
// Test Case 1.C — Property 3: Bug Condition — `/category-trend` returns one
//                              row per (report_date, category) pair.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
//   The query `GROUP BY cu.id, cu.report_date, category` keeps `cu.id` in
//   the grouping, so three uploads for 2025-05-11 each produce their own
//   (date, category) rows. With items in two categories, the response
//   contains 3 × 2 = 6 rows for 2025-05-11 instead of 2.
//
// **Validates: Requirements 1.6, 1.7**
//
describe('Bug Condition / Property 3 — GET /category-trend returns one row per (date, category)', () => {
    it('1.C canonical fixture — exactly one row per (report_date, category) and counts equal the SUM across uploads sharing the date', async () => {
        const { uploads, items } = fixtureMultiVerticalSingleDate();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/category-trend');
        expect(res.statusCode).toBe(200);

        // (1) Exactly one row for ('2025-05-11', 'Patching')
        const patching = res.body.categoryTrend.filter(c =>
            c.report_date === '2025-05-11' && c.category === 'Patching',
        );
        expect(patching).toHaveLength(1);

        // (2) That row's count equals the total compliance_items in
        //     'Patching' across every upload sharing the date.
        const expectedPatchingCount = items.filter(it =>
            uploads.some(u => u.id === it.upload_id && u.report_date === '2025-05-11') &&
            it.category === 'Patching',
        ).length;
        expect(patching[0].count).toBe(expectedPatchingCount);

        // (3) Same for 'Configuration'.
        const configuration = res.body.categoryTrend.filter(c =>
            c.report_date === '2025-05-11' && c.category === 'Configuration',
        );
        expect(configuration).toHaveLength(1);
        const expectedConfigCount = items.filter(it =>
            uploads.some(u => u.id === it.upload_id && u.report_date === '2025-05-11') &&
            it.category === 'Configuration',
        ).length;
        expect(configuration[0].count).toBe(expectedConfigCount);
    });

    it('1.C property — for any random multi-vertical scenario, every (date, category) appears at most once', async () => {
        await fc.assert(
            fc.asyncProperty(arbScenarioWithItems, async ({ uploads, items }) => {
                installReadEndpointHandler(uploads, items);
                const res = await request(server, 'GET', '/api/compliance/category-trend');
                expect(res.statusCode).toBe(200);

                // (1) Each (date, category) pair appears at most once.
                const seen = new Set();
                for (const row of res.body.categoryTrend) {
                    const key = `${row.report_date}|${row.category}`;
                    expect(seen.has(key)).toBe(false);
                    seen.add(key);
                }

                // (2) For every (date, category) pair, the count equals the
                //     SUM of compliance_items in that category across all
                //     uploads sharing the date.
                for (const row of res.body.categoryTrend) {
                    const expected = items.filter(it => {
                        const u = uploads.find(x => x.id === it.upload_id);
                        return u && u.report_date === row.report_date &&
                               (it.category || 'Unknown') === row.category;
                    }).length;
                    expect(row.count).toBe(expected);
                }
            }),
            { numRuns: 25 },
        );
    });
});

// =============================================================================
// Test Case 1.D — Property 4: Bug Condition — `/summary` does not silently
//                              drop sibling uploads.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
//   With three uploads for 2025-05-11 (NTS_AEO, SDIT_CISO, TSI), the query
//   `WHERE vertical IS NULL ORDER BY id DESC LIMIT 1` returns nothing
//   (none have vertical=null), so the fallback `WHERE vertical = 'NTS_AEO'`
//   selects only the NTS_AEO upload. The other two verticals' summary_json
//   is silently dropped — no entries are merged AND no
//   `multi_vertical_uploads` field exists on the response.
//
//   The fix exposes a `multi_vertical_uploads` array (option (b) per
//   design.md Fix 4); option (a) would merge entries from every sibling
//   upload. Either of these resolves the bug.
//
// **Validates: Requirements 1.8, 1.9**
//
describe('Bug Condition / Property 4 — GET /summary discloses or merges sibling uploads sharing the latest report_date', () => {
    it('1.D canonical fixture — response merges sibling entries OR exposes a non-empty multi_vertical_uploads array of length 2', async () => {
        const { uploads, items } = fixtureMultiVerticalSingleDate();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/summary');
        expect(res.statusCode).toBe(200);

        // The selected primary upload (NTS_AEO via fallback). Sibling uploads
        // are SDIT_CISO and TSI for the same report_date.
        const primary = uploads.find(u => u.vertical === 'NTS_AEO');
        const expectedSiblings = uploads
            .filter(u => u.report_date === primary.report_date && u.id !== primary.id)
            .sort((a, b) => a.id - b.id);

        const mergedAllEntries =
            Array.isArray(res.body.entries) &&
            res.body.entries.length === uploads.length;

        const disclosedSiblings =
            Array.isArray(res.body.multi_vertical_uploads) &&
            res.body.multi_vertical_uploads.length === expectedSiblings.length;

        // The bug exists iff neither disclosure mechanism is in place.
        expect(mergedAllEntries || disclosedSiblings).toBe(true);

        // If the response exposes multi_vertical_uploads, validate its shape.
        if (disclosedSiblings) {
            expect(res.body.multi_vertical_uploads).toHaveLength(2);
            const ids = res.body.multi_vertical_uploads.map(s => s.id).sort();
            expect(ids).toEqual(expectedSiblings.map(s => s.id).sort());
            const verticals = res.body.multi_vertical_uploads.map(s => s.vertical).sort();
            expect(verticals).toEqual(expectedSiblings.map(s => s.vertical).sort());
        }
    });

    it('1.D property — when two or more uploads share the latest report_date, the response merges entries OR discloses every sibling', async () => {
        await fc.assert(
            fc.asyncProperty(arbScenario, async (uploads) => {
                // Need at least one upload to have non-null summary_json AND
                // be selectable by the existing fallback (vertical IS NULL or
                // vertical = 'NTS_AEO'). Skip scenarios that do not exercise
                // the /summary code path.
                const selectable = uploads.find(u =>
                    (u.vertical == null || u.vertical === 'NTS_AEO') && u.summary_json,
                );
                fc.pre(selectable !== undefined);

                installReadEndpointHandler(uploads, []);
                const res = await request(server, 'GET', '/api/compliance/summary');
                expect(res.statusCode).toBe(200);

                if (!res.body.upload) {
                    // No primary upload selected → nothing to disclose.
                    return;
                }

                const primaryDate = res.body.upload.report_date;
                const siblings = uploads.filter(u =>
                    u.report_date === primaryDate && u.id !== res.body.upload.id,
                );

                if (siblings.length === 0) {
                    // No siblings exist → no disclosure required.
                    return;
                }

                const mergedAllEntries =
                    Array.isArray(res.body.entries) &&
                    res.body.entries.length >= 1 + siblings.length;
                const disclosedSiblings =
                    Array.isArray(res.body.multi_vertical_uploads) &&
                    res.body.multi_vertical_uploads.length === siblings.length;

                expect(mergedAllEntries || disclosedSiblings).toBe(true);
            }),
            { numRuns: 25 },
        );
    });
});

// =============================================================================
// Test Case 1.E — Property 5: Bug Condition — `persistUpload()` snapshot
//                              reflects only the snapshotted vertical.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
//   compliance_items has 100 NTS_AEO hosts on team STEAM and 50 SDIT_CISO
//   hosts on team ACCESS-ENG. A new SDIT_CISO upload (one item on STEAM,
//   distinct hostname) is persisted. The snapshot query has no vertical
//   filter and uses `team AS vertical`, so it produces:
//     - vertical = 'STEAM'      → total_devices = 101 (100 NTS_AEO + 1 new)
//     - vertical = 'ACCESS-ENG' → total_devices = 50  (all SDIT_CISO)
//   No row exists for vertical='SDIT_CISO', and the row for STEAM is
//   contaminated with 100 NTS_AEO hosts. The fix filters by the upload's
//   vertical (`WHERE vertical IS NOT DISTINCT FROM $1`) and groups by
//   (vertical, team), producing one snapshot row for SDIT_CISO whose
//   total_devices reflects only SDIT_CISO hosts.
//
// **Validates: Requirements 1.10, 1.11**
//
describe('Bug Condition / Property 5 — persistUpload() snapshot is filtered to the snapshotted vertical', () => {
    it('1.E canonical fixture — compliance_snapshots row for SDIT_CISO reflects only SDIT_CISO items, not NTS_AEO items', async () => {
        const items = fixtureCrossVerticalItems(); // 100 NTS_AEO STEAM + 50 SDIT_CISO ACCESS-ENG
        const incomingItem = {
            hostname: 'sdit-ciso-new-host-1',
            ip_address: '10.0.0.1',
            device_type: 'srv',
            team: 'STEAM',
            metric_id: 'M-NEW',
            metric_desc: 'desc',
            category: 'Patching',
            extra_json: {},
        };
        const incomingVertical = 'SDIT_CISO';

        const snapshotInserts = [];

        // The pool.query handler must respond to:
        //   1. SELECT id, hostname, metric_id, ... FROM compliance_items WHERE status = 'active'
        //   2. The snapshot SELECT (unfixed: no vertical filter; fixed: vertical filter)
        //   3. INSERT INTO compliance_snapshots ... ON CONFLICT ... DO UPDATE
        //      → captured into snapshotInserts
        queryHandler = makeQueryHandler([
            // (1) Initial active-items load
            {
                match: /SELECT\s+id,\s+hostname,\s+metric_id,\s+seen_count,\s+first_seen_upload_id\s+FROM\s+compliance_items\s+WHERE\s+status\s*=\s*'active'/i,
                rows: () => items.map(it => ({
                    id: it.id,
                    hostname: it.hostname,
                    metric_id: 'M-EXISTING',
                    seen_count: 1,
                    first_seen_upload_id: 1,
                })),
            },
            // (2a) UNFIXED snapshot query: SELECT team AS vertical ... FROM compliance_items WHERE team IS NOT NULL GROUP BY team
            {
                match: /SELECT\s+team\s+AS\s+vertical[\s\S]*FROM\s+compliance_items[\s\S]*WHERE\s+team\s+IS\s+NOT\s+NULL[\s\S]*GROUP\s+BY\s+team/i,
                rows: () => {
                    // Aggregate ALL items by team (regardless of vertical) — bug condition.
                    const byTeam = {};
                    const allHosts = items.concat([{
                        hostname: incomingItem.hostname,
                        team: incomingItem.team,
                        vertical: incomingVertical,
                        status: 'active',
                    }]);
                    for (const it of allHosts) {
                        if (!it.team) continue;
                        if (!byTeam[it.team]) {
                            byTeam[it.team] = { vertical: it.team, hosts: new Set(), compliant: new Set(), nonCompliant: new Set() };
                        }
                        byTeam[it.team].hosts.add(it.hostname);
                        if (it.status === 'resolved') byTeam[it.team].compliant.add(it.hostname);
                        if (it.status === 'active')   byTeam[it.team].nonCompliant.add(it.hostname);
                    }
                    return Object.values(byTeam).map(b => ({
                        vertical: b.vertical,
                        total_devices: b.hosts.size,
                        compliant: b.compliant.size,
                        non_compliant: b.nonCompliant.size,
                    }));
                },
            },
            // (2b) FIXED snapshot query: ... WHERE team IS NOT NULL AND vertical IS NOT DISTINCT FROM $1 GROUP BY vertical, team
            {
                match: /WHERE\s+team\s+IS\s+NOT\s+NULL\s+AND\s+vertical\s+IS\s+NOT\s+DISTINCT\s+FROM/i,
                rows: (_text, params) => {
                    const filterVertical = (params || [])[0];
                    const allHosts = items.concat([{
                        hostname: incomingItem.hostname,
                        team: incomingItem.team,
                        vertical: incomingVertical,
                        status: 'active',
                    }]);
                    const filtered = allHosts.filter(it => {
                        if (filterVertical == null) return it.vertical == null;
                        return it.vertical === filterVertical;
                    });
                    const byKey = {};
                    for (const it of filtered) {
                        if (!it.team) continue;
                        const k = `${it.vertical}|${it.team}`;
                        if (!byKey[k]) {
                            byKey[k] = {
                                vertical: it.vertical,
                                team: it.team,
                                hosts: new Set(),
                                compliant: new Set(),
                                nonCompliant: new Set(),
                            };
                        }
                        byKey[k].hosts.add(it.hostname);
                        if (it.status === 'resolved') byKey[k].compliant.add(it.hostname);
                        if (it.status === 'active')   byKey[k].nonCompliant.add(it.hostname);
                    }
                    return Object.values(byKey).map(b => ({
                        vertical: b.vertical,
                        team: b.team,
                        total_devices: b.hosts.size,
                        compliant: b.compliant.size,
                        non_compliant: b.nonCompliant.size,
                    }));
                },
            },
            // (3) Snapshot upsert — capture every insert
            {
                match: /INSERT\s+INTO\s+compliance_snapshots/i,
                rows: (_text, params) => {
                    const [snapshot_month, vertical, total_devices, compliant, non_compliant, compliance_pct] = params || [];
                    snapshotInserts.push({ snapshot_month, vertical, total_devices, compliant, non_compliant, compliance_pct });
                    return [];
                },
            },
        ]);

        // The transactional client is also routed through queryHandler; it
        // must answer the within-transaction queries (BEGIN/COMMIT, INSERT
        // INTO compliance_uploads RETURNING id, item upserts, the resolved
        // updates, and the final upload counts UPDATE).
        const client = {
            query: jest.fn((text, params) => {
                if (/^\s*BEGIN/i.test(text) || /^\s*COMMIT/i.test(text) || /^\s*ROLLBACK/i.test(text)) {
                    return Promise.resolve({ rows: [], rowCount: 0 });
                }
                if (/INSERT\s+INTO\s+compliance_uploads[\s\S]*RETURNING\s+id/i.test(text)) {
                    return Promise.resolve({ rows: [{ id: 9999 }], rowCount: 1 });
                }
                // All other within-transaction writes succeed silently.
                return Promise.resolve({ rows: [], rowCount: 1 });
            }),
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(client);

        await persistUpload({
            items: [incomingItem],
            summary: { entries: [], overall_scores: {} },
            reportDate: '2025-05-11',
            filename: 'sdit-ciso-2025-05-11.xlsx',
            userId: 1,
            vertical: incomingVertical,
        });

        // (1) A snapshot row was written for vertical = 'SDIT_CISO'.
        const sditSnapshots = snapshotInserts.filter(s => s.vertical === 'SDIT_CISO');
        expect(sditSnapshots.length).toBeGreaterThan(0);

        // (2) Its total_devices reflects only SDIT_CISO items.
        //     Pre-existing SDIT_CISO hosts: 50 (on ACCESS-ENG team).
        //     Plus the one incoming SDIT_CISO host on STEAM.
        //     If the snapshot is grouped per (vertical, team), we expect
        //     either one row totalling 51 hosts or two rows that together
        //     total 51. Either way, total_devices for SDIT_CISO must NOT
        //     equal 151 (the inflated cross-vertical figure that includes
        //     the 100 NTS_AEO hosts).
        const sditTotal = sditSnapshots.reduce((s, r) => s + r.total_devices, 0);
        const ntsAeoHostCount = items.filter(it => it.vertical === 'NTS_AEO').length;
        expect(sditTotal).toBeLessThan(ntsAeoHostCount + 1);
        expect(sditTotal).toBe(50 + 1);

        // (3) No snapshot row inflates total_devices with NTS_AEO hosts.
        //     The unfixed code emits vertical='STEAM' with total=101
        //     (100 NTS_AEO + 1 new SDIT_CISO host). The fix emits per-vertical
        //     rows, so any STEAM row must reflect only items whose vertical
        //     is the snapshotted vertical.
        const steamSnapshots = snapshotInserts.filter(s => s.vertical === 'STEAM');
        for (const s of steamSnapshots) {
            // Under the fix, a snapshot row keyed on vertical='STEAM' should
            // not exist at all (the upload's vertical is SDIT_CISO). Even if
            // legacy code paths still write a STEAM row, it must not include
            // the 100 NTS_AEO hosts as a single combined total.
            expect(s.total_devices).toBeLessThan(ntsAeoHostCount);
        }
    });
});

// =============================================================================
// PRESERVATION TESTS (Task 2 — Property 2)
// =============================================================================
//
// These tests pin the BASELINE behavior of the unfixed code on inputs where
// the bug condition does NOT hold (single-upload-per-date scenarios, empty
// state, error paths, and unrelated query-parameter filtering). They MUST
// pass on the unfixed code; they will continue to pass after the five fixes
// land in tasks 3 through 7. Any future change that alters these responses
// in the non-bug-condition input space is a regression.
//
// Bug Condition negation (from design.md):
//   FORALL report_date d, COUNT(compliance_uploads WHERE report_date = d) <= 1
//
// **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10**
//

// --- Preservation fixtures (per design.md "Test Fixtures Required") ---

/**
 * fixture_empty — no compliance_uploads, no compliance_items.
 * Used by 2.A.
 */
function fixtureEmpty() {
    return { uploads: [], items: [] };
}

/**
 * fixture_single_upload_aeo_legacy — one legacy AEO upload (vertical IS NULL)
 * dated 2025-04-01 with 20 items distributed across the four ALLOWED_TEAMS.
 * Items are evenly tagged across two categories (Patching, Configuration).
 * Used by 2.B.
 */
function fixtureSingleUploadAeoLegacy() {
    const upload = {
        id: 100,
        report_date: '2025-04-01',
        vertical: null,
        new_count: 12,
        recurring_count: 8,
        resolved_count: 3,
        uploaded_at: '2025-04-01T09:00:00Z',
        summary_json: JSON.stringify({
            entries: [
                { team: 'STEAM',      metric: 'patching',      score: 82 },
                { team: 'ACCESS-ENG', metric: 'patching',      score: 76 },
                { team: 'ACCESS-OPS', metric: 'configuration', score: 88 },
                { team: 'INTELDEV',   metric: 'configuration', score: 91 },
            ],
            overall_scores: { patching: 79, configuration: 90 },
        }),
    };

    const teamLayout = ['STEAM', 'STEAM', 'STEAM', 'STEAM', 'STEAM',
                        'ACCESS-ENG', 'ACCESS-ENG', 'ACCESS-ENG', 'ACCESS-ENG', 'ACCESS-ENG',
                        'ACCESS-OPS', 'ACCESS-OPS', 'ACCESS-OPS', 'ACCESS-OPS', 'ACCESS-OPS',
                        'INTELDEV',   'INTELDEV',   'INTELDEV',   'INTELDEV',   'INTELDEV'];

    const items = teamLayout.map((team, i) => ({
        id: 4000 + i,
        upload_id: upload.id,
        hostname: `aeo-host-${i + 1}`,
        team,
        category: i % 2 === 0 ? 'Patching' : 'Configuration',
        vertical: null,
        status: 'active',
    }));

    return { uploads: [upload], items };
}

/**
 * fixture_single_upload_per_date — five uploads on five distinct dates with
 * varied vertical values, satisfying the bug-condition negation
 * (every report_date has exactly one upload row). 4–6 items per upload.
 * Used by 2.C.
 */
function fixtureSingleUploadPerDate() {
    const spec = [
        { id: 200, date: '2025-04-01', vertical: null,        new: 5,  rec: 3,  res: 1 },
        { id: 201, date: '2025-04-08', vertical: 'NTS_AEO',   new: 7,  rec: 2,  res: 4 },
        { id: 202, date: '2025-04-15', vertical: 'SDIT_CISO', new: 4,  rec: 6,  res: 2 },
        { id: 203, date: '2025-04-22', vertical: 'TSI',       new: 9,  rec: 1,  res: 0 },
        { id: 204, date: '2025-05-01', vertical: null,        new: 6,  rec: 4,  res: 5 },
    ];

    const uploads = spec.map(s => ({
        id: s.id,
        report_date: s.date,
        vertical: s.vertical,
        new_count: s.new,
        recurring_count: s.rec,
        resolved_count: s.res,
        uploaded_at: `${s.date}T08:00:00Z`,
        summary_json: JSON.stringify({
            entries: [{ team: 'STEAM', metric: 'patching', score: 80 + (s.id % 10) }],
            overall_scores: { patching: 80 + (s.id % 10) },
        }),
    }));

    const items = [];
    let itemId = 6000;
    for (const u of uploads) {
        const layout = [
            { team: 'STEAM',      category: 'Patching' },
            { team: 'STEAM',      category: 'Configuration' },
            { team: 'ACCESS-ENG', category: 'Patching' },
            { team: 'ACCESS-OPS', category: 'Configuration' },
            { team: 'INTELDEV',   category: 'Patching' },
        ];
        for (const l of layout) {
            items.push({
                id: itemId++,
                upload_id: u.id,
                hostname: `${u.vertical || 'AEO'}-host-${itemId}`,
                team: l.team,
                category: l.category,
                vertical: u.vertical,
                status: 'active',
            });
        }
    }
    return { uploads, items };
}

/**
 * fixture_cross_vertical_items_single — only NTS_AEO items present in
 * compliance_items (no SDIT_CISO/TSI items). Used by 2.E to exercise
 * the persistUpload() single-vertical-month preservation path.
 */
function fixtureSingleVerticalItems() {
    const items = [];
    let id = 7000;
    for (let i = 1; i <= 30; i++) {
        items.push({
            id: id++,
            hostname: `nts-aeo-only-host-${i}`,
            team: i % 2 === 0 ? 'STEAM' : 'ACCESS-ENG',
            vertical: 'NTS_AEO',
            status: i % 5 === 0 ? 'resolved' : 'active',
        });
    }
    return items;
}

// --- arbScenario_singleUploadPerDate — fast-check generator restricted to
//     scenarios where every report_date has exactly one upload row.        ---

const arbUniqueDate = fc.constantFrom(
    '2025-03-04', '2025-03-11', '2025-03-18', '2025-03-25',
    '2025-04-01', '2025-04-08', '2025-04-15', '2025-04-22',
    '2025-05-01', '2025-05-18', '2025-05-25',
);

const arbUploadUnique = fc.record({
    report_date:     arbUniqueDate,
    vertical:        arbVertical,
    new_count:       fc.integer({ min: 0, max: 30 }),
    recurring_count: fc.integer({ min: 0, max: 30 }),
    resolved_count:  fc.integer({ min: 0, max: 30 }),
});

/**
 * arbScenarioSingleUploadPerDate — list of compliance_uploads rows where
 * every report_date appears at most once. Enforced post-generation via a
 * filter; fast-check shrinking still finds simple counterexamples.
 */
const arbScenarioSingleUploadPerDate = fc.array(arbUploadUnique, { minLength: 0, maxLength: 6 })
    .filter(arr => {
        const dates = arr.map(u => u.report_date);
        return new Set(dates).size === dates.length;
    })
    .map((rawUploads) => rawUploads.map((u, i) => ({
        id: 8000 + i,
        uploaded_at: `${u.report_date}T${10 + i}:00:00Z`,
        summary_json: JSON.stringify({
            entries: [{ team: 'STEAM', metric: 'patching', score: 80 }],
            overall_scores: { patching: 80 },
        }),
        ...u,
    })));

// =============================================================================
// Test Case 2.A — Empty-state preservation
// =============================================================================
//
// With no compliance_uploads and no compliance_items, every read endpoint
// SHALL return its documented empty-state shape unchanged.
//
// **Validates: Requirements 3.3, 3.10**
//
describe('Preservation 2.A — empty-state response shapes are unchanged', () => {
    it('GET /trends returns { trends: [] } when no uploads exist', async () => {
        const { uploads, items } = fixtureEmpty();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/trends');
        expect(res.statusCode).toBe(200);
        expect(res.body).toEqual({ trends: [] });
    });

    it('GET /top-recurring returns { waterfall: [] } when no uploads exist', async () => {
        const { uploads, items } = fixtureEmpty();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/top-recurring');
        expect(res.statusCode).toBe(200);
        expect(res.body).toEqual({ waterfall: [] });
    });

    it('GET /category-trend returns { categoryTrend: [] } when no uploads exist', async () => {
        const { uploads, items } = fixtureEmpty();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/category-trend');
        expect(res.statusCode).toBe(200);
        expect(res.body).toEqual({ categoryTrend: [] });
    });

    it('GET /summary returns { entries: [], overall_scores: {}, upload: null } when no uploads exist', async () => {
        const { uploads, items } = fixtureEmpty();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/summary');
        expect(res.statusCode).toBe(200);
        expect(res.body).toEqual({ entries: [], overall_scores: {}, upload: null });
    });
});

// =============================================================================
// Test Case 2.B — Single AEO-legacy-upload preservation
// =============================================================================
//
// One legacy upload with vertical IS NULL. The four read endpoints SHALL
// produce responses that match the captured baseline byte-for-byte.
// This is the "snapshot equality — single AEO-only upload" case from
// design.md "Preservation Checking → Test Cases".
//
// **Validates: Requirements 3.1, 3.2, 3.4, 3.5, 3.6**
//
describe('Preservation 2.B — single AEO-legacy upload responses are byte-for-byte stable', () => {
    it('GET /trends returns one entry for the legacy upload with full per-team breakdown', async () => {
        const { uploads, items } = fixtureSingleUploadAeoLegacy();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/trends');
        expect(res.statusCode).toBe(200);

        // The legacy fixture has 5 items per team across 4 teams = 20 items.
        // Per-team breakdown is captured exactly so any future change that
        // alters this aggregation surfaces as a regression.
        expect(res.body).toEqual({
            trends: [
                {
                    report_date: '2025-04-01',
                    new_count: 12,
                    recurring_count: 8,
                    resolved_count: 3,
                    total_active: 20,
                    STEAM: 5,
                    'ACCESS-ENG': 5,
                    'ACCESS-OPS': 5,
                    INTELDEV: 5,
                },
            ],
        });
    });

    it('GET /top-recurring returns a single waterfall entry with start=0 and correct end', async () => {
        const { uploads, items } = fixtureSingleUploadAeoLegacy();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/top-recurring');
        expect(res.statusCode).toBe(200);

        // For a single legacy upload, the running-total invariant collapses
        // to start=0, end = new + recurring - resolved = 12 + 8 - 3 = 17.
        expect(res.body).toEqual({
            waterfall: [
                {
                    date: '2025-04-01',
                    start: 0,
                    new_count: 12,
                    recurring_count: 8,
                    resolved_count: 3,
                    end: 17,
                },
            ],
        });
    });

    it('GET /category-trend returns one row per category with the legacy upload counts', async () => {
        const { uploads, items } = fixtureSingleUploadAeoLegacy();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/category-trend');
        expect(res.statusCode).toBe(200);

        // 20 items total split across two categories: items at even indexes
        // are 'Patching' (10 items), odd indexes are 'Configuration' (10 items).
        expect(res.body).toEqual({
            categoryTrend: [
                { report_date: '2025-04-01', category: 'Configuration', count: 10 },
                { report_date: '2025-04-01', category: 'Patching',      count: 10 },
            ],
        });
    });

    it('GET /summary returns the legacy upload`s entries and overall_scores via the vertical IS NULL fallback', async () => {
        const { uploads, items } = fixtureSingleUploadAeoLegacy();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/summary');
        expect(res.statusCode).toBe(200);

        // The vertical IS NULL → vertical = 'NTS_AEO' fallback selects the
        // legacy upload directly (vertical IS NULL succeeds first). The
        // response surfaces summary.entries and summary.overall_scores
        // unchanged plus a stub upload reference.
        expect(res.body).toEqual({
            entries: [
                { team: 'STEAM',      metric: 'patching',      score: 82 },
                { team: 'ACCESS-ENG', metric: 'patching',      score: 76 },
                { team: 'ACCESS-OPS', metric: 'configuration', score: 88 },
                { team: 'INTELDEV',   metric: 'configuration', score: 91 },
            ],
            overall_scores: { patching: 79, configuration: 90 },
            upload: {
                id: 100,
                report_date: '2025-04-01',
                uploaded_at: '2025-04-01T09:00:00Z',
            },
        });
    });
});

// =============================================================================
// Test Case 2.C — Multiple single-upload-per-date preservation
// =============================================================================
//
// Five distinct dates, one upload per date, varied vertical values. Every
// read endpoint SHALL produce results identical to the captured baseline.
// Bug condition negation holds (no two uploads share a report_date).
//
// **Validates: Requirements 3.1, 3.4, 3.5**
//
describe('Preservation 2.C — multiple single-upload-per-date responses are byte-for-byte stable', () => {
    it('GET /trends returns one entry per date, ordered by report_date ASC, with correct per-team breakdowns', async () => {
        const { uploads, items } = fixtureSingleUploadPerDate();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/trends');
        expect(res.statusCode).toBe(200);

        // Each upload has 5 items in the layout (2 STEAM, 1 ACCESS-ENG,
        // 1 ACCESS-OPS, 1 INTELDEV). With one upload per date, every date
        // produces an identical per-team breakdown.
        const expectedTrends = uploads.map(u => ({
            report_date: u.report_date,
            new_count: u.new_count,
            recurring_count: u.recurring_count,
            resolved_count: u.resolved_count,
            total_active: u.new_count + u.recurring_count,
            STEAM: 2,
            'ACCESS-ENG': 1,
            'ACCESS-OPS': 1,
            INTELDEV: 1,
        }));
        expect(res.body).toEqual({ trends: expectedTrends });
    });

    it('GET /top-recurring emits one waterfall entry per date with start carrying forward from previous end', async () => {
        const { uploads, items } = fixtureSingleUploadPerDate();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/top-recurring');
        expect(res.statusCode).toBe(200);

        // Build the expected waterfall by walking uploads in date order and
        // carrying `start` forward. Single-upload-per-date is the canonical
        // case computeWaterfall() was designed for; the running-total
        // semantics MUST match the pre-fix output exactly.
        let start = 0;
        const expectedWaterfall = uploads.map(u => {
            const end = start + u.new_count + u.recurring_count - u.resolved_count;
            const entry = {
                date: u.report_date,
                start,
                new_count: u.new_count,
                recurring_count: u.recurring_count,
                resolved_count: u.resolved_count,
                end,
            };
            start = end;
            return entry;
        });
        expect(res.body).toEqual({ waterfall: expectedWaterfall });
    });

    it('GET /category-trend returns one row per (date, category), ordered by date then category', async () => {
        const { uploads, items } = fixtureSingleUploadPerDate();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/category-trend');
        expect(res.statusCode).toBe(200);

        // Each upload has 3 items in 'Patching' (indexes 0, 2, 4) and
        // 2 items in 'Configuration' (indexes 1, 3). Single-upload-per-date
        // means each (date, category) pair appears exactly once.
        const expected = [];
        for (const u of uploads) {
            expected.push({ report_date: u.report_date, category: 'Configuration', count: 2 });
            expected.push({ report_date: u.report_date, category: 'Patching',      count: 3 });
        }
        expect(res.body).toEqual({ categoryTrend: expected });
    });

    it('GET /summary surfaces the latest legacy/NTS_AEO upload via the existing fallback', async () => {
        const { uploads, items } = fixtureSingleUploadPerDate();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/summary');
        expect(res.statusCode).toBe(200);

        // The fallback prefers vertical IS NULL with the highest id. In this
        // fixture, the highest-id upload with vertical IS NULL is id=204
        // (date 2025-05-01).
        const primary = uploads.find(u => u.id === 204);
        expect(res.body).toEqual({
            entries: [{ team: 'STEAM', metric: 'patching', score: 80 + (204 % 10) }],
            overall_scores: { patching: 80 + (204 % 10) },
            upload: {
                id: primary.id,
                report_date: primary.report_date,
                uploaded_at: primary.uploaded_at,
            },
        });
    });
});

// =============================================================================
// Test Case 2.D — /summary `team` query parameter preservation
// =============================================================================
//
// The `team` query parameter still filters `entries` server-side and still
// rejects non-ALLOWED_TEAMS values with HTTP 400.
//
// **Validates: Requirement 3.7**
//
describe('Preservation 2.D — GET /summary `team` query parameter is unchanged', () => {
    it('?team=STEAM filters entries to STEAM only', async () => {
        const { uploads, items } = fixtureSingleUploadAeoLegacy();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/summary?team=STEAM');
        expect(res.statusCode).toBe(200);
        expect(res.body.entries).toEqual([
            { team: 'STEAM', metric: 'patching', score: 82 },
        ]);
        // The unrelated overall_scores and upload fields remain unchanged
        // when filtering by team.
        expect(res.body.overall_scores).toEqual({ patching: 79, configuration: 90 });
        expect(res.body.upload).toEqual({
            id: 100,
            report_date: '2025-04-01',
            uploaded_at: '2025-04-01T09:00:00Z',
        });
    });

    it('?team=OTHER (not in ALLOWED_TEAMS) returns HTTP 400 with { error }', async () => {
        const { uploads, items } = fixtureSingleUploadAeoLegacy();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/summary?team=OTHER');
        expect(res.statusCode).toBe(400);
        expect(res.body).toEqual({ error: 'Invalid team' });
    });

    it('?team=ACCESS-ENG filters entries to ACCESS-ENG only', async () => {
        const { uploads, items } = fixtureSingleUploadAeoLegacy();
        installReadEndpointHandler(uploads, items);

        const res = await request(server, 'GET', '/api/compliance/summary?team=ACCESS-ENG');
        expect(res.statusCode).toBe(200);
        expect(res.body.entries).toEqual([
            { team: 'ACCESS-ENG', metric: 'patching', score: 76 },
        ]);
    });
});

// =============================================================================
// Test Case 2.E — persistUpload() single-vertical-month preservation
// =============================================================================
//
// When compliance_items contains rows from only one vertical, the snapshot
// rows written by persistUpload() SHALL be identical to the pre-fix output.
// This is the single-vertical-month equivalent of the fixed-code expectation
// that snapshots reflect only the snapshotted vertical.
//
// **Validates: Requirement 3.8**
//
describe('Preservation 2.E — persistUpload() snapshot is unchanged for single-vertical months', () => {
    it('snapshot rows reflect the single-vertical compliance_items state with no cross-contamination', async () => {
        const items = fixtureSingleVerticalItems(); // only NTS_AEO items
        const incomingItem = {
            hostname: 'nts-aeo-new-host-1',
            ip_address: '10.0.0.1',
            device_type: 'srv',
            team: 'STEAM',
            metric_id: 'M-NEW',
            metric_desc: 'desc',
            category: 'Patching',
            extra_json: {},
        };
        const incomingVertical = 'NTS_AEO';

        const snapshotInserts = [];

        queryHandler = makeQueryHandler([
            // (1) Initial active-items load
            {
                match: /SELECT\s+id,\s+hostname,\s+metric_id,\s+seen_count,\s+first_seen_upload_id\s+FROM\s+compliance_items\s+WHERE\s+status\s*=\s*'active'/i,
                rows: () => items
                    .filter(it => it.status === 'active')
                    .map(it => ({
                        id: it.id,
                        hostname: it.hostname,
                        metric_id: 'M-EXISTING',
                        seen_count: 1,
                        first_seen_upload_id: 1,
                    })),
            },
            // (2a) UNFIXED snapshot query: SELECT team AS vertical ... GROUP BY team
            {
                match: /SELECT\s+team\s+AS\s+vertical[\s\S]*FROM\s+compliance_items[\s\S]*WHERE\s+team\s+IS\s+NOT\s+NULL[\s\S]*GROUP\s+BY\s+team/i,
                rows: () => {
                    // Single-vertical month: aggregating ALL items by team
                    // gives the same per-vertical answer as filtering by
                    // the snapshotted vertical, because no other verticals
                    // contribute. This is the preservation case.
                    const allHosts = items.concat([{
                        hostname: incomingItem.hostname,
                        team: incomingItem.team,
                        vertical: incomingVertical,
                        status: 'active',
                    }]);
                    const byTeam = {};
                    for (const it of allHosts) {
                        if (!it.team) continue;
                        if (!byTeam[it.team]) {
                            byTeam[it.team] = { vertical: it.team, hosts: new Set(), compliant: new Set(), nonCompliant: new Set() };
                        }
                        byTeam[it.team].hosts.add(it.hostname);
                        if (it.status === 'resolved') byTeam[it.team].compliant.add(it.hostname);
                        if (it.status === 'active')   byTeam[it.team].nonCompliant.add(it.hostname);
                    }
                    return Object.values(byTeam).map(b => ({
                        vertical: b.vertical,
                        total_devices: b.hosts.size,
                        compliant: b.compliant.size,
                        non_compliant: b.nonCompliant.size,
                    }));
                },
            },
            // (2b) FIXED snapshot query: ... WHERE team IS NOT NULL AND vertical IS NOT DISTINCT FROM $1 GROUP BY vertical, team
            //
            // The /commit route calls persistUpload() without a vertical
            // argument (legacy AEO uploads default to vertical = null), so
            // the fixed SQL filters items via `vertical IS NOT DISTINCT FROM
            // null`. For a single-vertical month, this is equivalent to
            // aggregating by team alone — every contributing item shares the
            // same (null) vertical bucket from the upload's perspective.
            //
            // Returning rows with `vertical: null, team: <team>` makes the
            // production INSERT loop's `vs.vertical || vs.team` fallback
            // resolve to the team name, which preserves the historical
            // team-as-vertical snapshot key for legacy AEO upload contexts.
            {
                match: /WHERE\s+team\s+IS\s+NOT\s+NULL\s+AND\s+vertical\s+IS\s+NOT\s+DISTINCT\s+FROM/i,
                rows: () => {
                    const allHosts = items.concat([{
                        hostname: incomingItem.hostname,
                        team: incomingItem.team,
                        vertical: incomingVertical,
                        status: 'active',
                    }]);
                    const byTeam = {};
                    for (const it of allHosts) {
                        if (!it.team) continue;
                        if (!byTeam[it.team]) {
                            byTeam[it.team] = {
                                team: it.team,
                                hosts: new Set(),
                                compliant: new Set(),
                                nonCompliant: new Set(),
                            };
                        }
                        byTeam[it.team].hosts.add(it.hostname);
                        if (it.status === 'resolved') byTeam[it.team].compliant.add(it.hostname);
                        if (it.status === 'active')   byTeam[it.team].nonCompliant.add(it.hostname);
                    }
                    return Object.values(byTeam).map(b => ({
                        vertical: null,
                        team: b.team,
                        total_devices: b.hosts.size,
                        compliant: b.compliant.size,
                        non_compliant: b.nonCompliant.size,
                    }));
                },
            },
            // (3) Snapshot upsert — capture every insert
            {
                match: /INSERT\s+INTO\s+compliance_snapshots/i,
                rows: (_text, params) => {
                    const [snapshot_month, vertical, total_devices, compliant, non_compliant, compliance_pct] = params || [];
                    snapshotInserts.push({ snapshot_month, vertical, total_devices, compliant, non_compliant, compliance_pct });
                    return [];
                },
            },
        ]);

        const client = {
            query: jest.fn((text, _params) => {
                if (/^\s*BEGIN/i.test(text) || /^\s*COMMIT/i.test(text) || /^\s*ROLLBACK/i.test(text)) {
                    return Promise.resolve({ rows: [], rowCount: 0 });
                }
                if (/INSERT\s+INTO\s+compliance_uploads[\s\S]*RETURNING\s+id/i.test(text)) {
                    return Promise.resolve({ rows: [{ id: 9999 }], rowCount: 1 });
                }
                return Promise.resolve({ rows: [], rowCount: 1 });
            }),
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(client);

        const result = await persistUpload({
            items: [incomingItem],
            summary: { entries: [], overall_scores: {} },
            reportDate: '2025-04-01',
            filename: 'nts-aeo-only-2025-04-01.xlsx',
            userId: 1,
        });

        // (1) Upload commit succeeded with the expected uploadId.
        expect(result.uploadId).toBe(9999);

        // (2) Snapshot rows were written. With only NTS_AEO items present,
        //     the per-team aggregation gives:
        //       - team='STEAM'      → all 15 NTS_AEO STEAM hosts (even ids
        //         in the fixture, indexes 2..30 = 15 hosts) plus the new
        //         incoming STEAM host = 16 total.
        //       - team='ACCESS-ENG' → 15 NTS_AEO ACCESS-ENG hosts.
        const steamSnap = snapshotInserts.find(s => s.vertical === 'STEAM');
        const accessSnap = snapshotInserts.find(s => s.vertical === 'ACCESS-ENG');

        expect(steamSnap).toBeDefined();
        expect(accessSnap).toBeDefined();

        // STEAM bucket: NTS_AEO indexes 2,4,...,30 = 15 hosts + the new one.
        expect(steamSnap.total_devices).toBe(16);
        // ACCESS-ENG bucket: NTS_AEO indexes 1,3,...,29 = 15 hosts.
        expect(accessSnap.total_devices).toBe(15);

        // (3) compliance_pct is consistent with the captured baseline:
        //     resolved hosts are at indexes divisible by 5 (5,10,15,20,25,30).
        //     STEAM: 10, 20, 30 → 3 resolved out of 16 → 18.75%.
        //     ACCESS-ENG: 5, 15, 25 → 3 resolved out of 15 → 20%.
        expect(steamSnap.compliant).toBe(3);
        expect(steamSnap.non_compliant).toBe(13); // 16 - 3 resolved = 13 active
        expect(steamSnap.compliance_pct).toBe(18.75);

        expect(accessSnap.compliant).toBe(3);
        expect(accessSnap.non_compliant).toBe(12);
        expect(accessSnap.compliance_pct).toBe(20);
    });
});

// =============================================================================
// Test Case 2.F — persistUpload() snapshot error-path preservation
// =============================================================================
//
// When the snapshot SELECT query fails, persistUpload() SHALL still commit
// the upload, log the error to console.error, and not surface the error to
// the caller. The HTTP layer SHALL NOT respond with an error status.
//
// **Validates: Requirement 3.9**
//
describe('Preservation 2.F — persistUpload() commits the upload when snapshot creation fails', () => {
    it('snapshot query rejection is caught — upload commits, error is logged, no error is thrown to caller', async () => {
        const incomingItem = {
            hostname: 'preserve-host-1',
            ip_address: '10.0.0.99',
            device_type: 'srv',
            team: 'STEAM',
            metric_id: 'M-PRESERVE',
            metric_desc: 'desc',
            category: 'Patching',
            extra_json: {},
        };

        const snapshotInserts = [];

        queryHandler = makeQueryHandler([
            // (1) Initial active-items load — empty
            {
                match: /SELECT\s+id,\s+hostname,\s+metric_id,\s+seen_count,\s+first_seen_upload_id\s+FROM\s+compliance_items\s+WHERE\s+status\s*=\s*'active'/i,
                rows: () => [],
            },
            // (2) Snapshot SELECT — REJECT to force the error path. We use
            //     a sentinel marker via a function-rows handler that throws.
            //     The makeQueryHandler infrastructure expects a row array, so
            //     we drop in a row provider that returns a Promise rejection
            //     by overriding the handler below.
        ]);

        // Wrap the existing handler so the snapshot SELECT rejects.
        const baseHandler = queryHandler;
        queryHandler = (text, params) => {
            if (/SELECT\s+team\s+AS\s+vertical[\s\S]*FROM\s+compliance_items[\s\S]*WHERE\s+team\s+IS\s+NOT\s+NULL[\s\S]*GROUP\s+BY\s+team/i.test(text)
                || /WHERE\s+team\s+IS\s+NOT\s+NULL\s+AND\s+vertical\s+IS\s+NOT\s+DISTINCT\s+FROM/i.test(text)) {
                return Promise.reject(new Error('simulated snapshot query failure'));
            }
            if (/INSERT\s+INTO\s+compliance_snapshots/i.test(text)) {
                const [snapshot_month, vertical, total_devices, compliant, non_compliant, compliance_pct] = params || [];
                snapshotInserts.push({ snapshot_month, vertical, total_devices, compliant, non_compliant, compliance_pct });
                return Promise.resolve({ rows: [], rowCount: 0 });
            }
            return baseHandler(text, params);
        };

        const client = {
            query: jest.fn((text, _params) => {
                if (/^\s*BEGIN/i.test(text) || /^\s*COMMIT/i.test(text) || /^\s*ROLLBACK/i.test(text)) {
                    return Promise.resolve({ rows: [], rowCount: 0 });
                }
                if (/INSERT\s+INTO\s+compliance_uploads[\s\S]*RETURNING\s+id/i.test(text)) {
                    return Promise.resolve({ rows: [{ id: 4242 }], rowCount: 1 });
                }
                return Promise.resolve({ rows: [], rowCount: 1 });
            }),
            release: jest.fn(),
        };
        mockPool.connect.mockResolvedValueOnce(client);

        // Silence the expected console.error so the test output stays clean.
        const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

        let result;
        let thrown;
        try {
            result = await persistUpload({
                items: [incomingItem],
                summary: { entries: [], overall_scores: {} },
                reportDate: '2025-04-01',
                filename: 'preserve-error-2025-04-01.xlsx',
                userId: 1,
            });
        } catch (err) {
            thrown = err;
        }

        // (1) persistUpload() did NOT throw — the snapshot error is swallowed.
        expect(thrown).toBeUndefined();

        // (2) Upload commit returned the expected payload.
        expect(result).toBeDefined();
        expect(result.uploadId).toBe(4242);
        expect(result.newCount).toBe(1);
        expect(result.recurringCount).toBe(0);
        expect(result.resolvedCount).toBe(0);

        // (3) The transaction reached COMMIT (not ROLLBACK) before the
        //     snapshot block ran. Inspect the client.query mock for COMMIT.
        const commitCalls = client.query.mock.calls.filter(c => /^\s*COMMIT/i.test(c[0]));
        const rollbackCalls = client.query.mock.calls.filter(c => /^\s*ROLLBACK/i.test(c[0]));
        expect(commitCalls.length).toBe(1);
        expect(rollbackCalls.length).toBe(0);

        // (4) The error WAS logged (preserves the existing error-path
        //     observability contract).
        expect(errSpy).toHaveBeenCalled();
        const loggedFirstArg = errSpy.mock.calls.map(c => String(c[0])).join('|');
        expect(loggedFirstArg).toContain('[Compliance] Snapshot creation error:');

        // (5) No INSERT INTO compliance_snapshots fired because the SELECT
        //     rejected before the upsert loop.
        expect(snapshotInserts).toHaveLength(0);

        errSpy.mockRestore();
    });
});

// =============================================================================
// Cross-endpoint preservation property — fast-check extension of Task 2
// =============================================================================
//
// For any randomly generated scenario where every report_date has exactly
// one upload row (the bug-condition negation), the four read endpoints
// SHALL satisfy the same shape and equality predicates that the captured
// baseline establishes for the canonical fixtures: one trend per date,
// one waterfall entry per date with the running invariant, one
// (date, category) row per pair, and a /summary with a non-null `upload`
// when any selectable upload exists.
//
// **Validates: Requirements 3.1, 3.2, 3.4, 3.5**
//
describe('Preservation cross-endpoint property — single-upload-per-date scenarios match the baseline shape', () => {
    it('GET /trends produces one entry per report_date, in date-ascending order, with counts equal to the per-date upload values', async () => {
        await fc.assert(
            fc.asyncProperty(arbScenarioSingleUploadPerDate, async (uploads) => {
                installReadEndpointHandler(uploads, []);
                const res = await request(server, 'GET', '/api/compliance/trends');
                expect(res.statusCode).toBe(200);

                expect(res.body.trends).toHaveLength(uploads.length);

                // Order: report_date ascending.
                const sorted = [...uploads].sort((a, b) =>
                    a.report_date.localeCompare(b.report_date),
                );
                for (let i = 0; i < sorted.length; i++) {
                    const u = sorted[i];
                    const t = res.body.trends[i];
                    expect(t.report_date).toBe(u.report_date);
                    expect(t.new_count).toBe(u.new_count);
                    expect(t.recurring_count).toBe(u.recurring_count);
                    expect(t.resolved_count).toBe(u.resolved_count);
                    expect(t.total_active).toBe(u.new_count + u.recurring_count);
                    // No items in this scenario → all per-team counts are 0.
                    expect(t.STEAM).toBe(0);
                    expect(t['ACCESS-ENG']).toBe(0);
                    expect(t['ACCESS-OPS']).toBe(0);
                    expect(t.INTELDEV).toBe(0);
                }
            }),
            { numRuns: 25 },
        );
    });

    it('GET /top-recurring produces one waterfall entry per report_date with the running invariant carried forward', async () => {
        await fc.assert(
            fc.asyncProperty(arbScenarioSingleUploadPerDate, async (uploads) => {
                installReadEndpointHandler(uploads, []);
                const res = await request(server, 'GET', '/api/compliance/top-recurring');
                expect(res.statusCode).toBe(200);

                const wf = res.body.waterfall;
                expect(wf).toHaveLength(uploads.length);

                if (wf.length > 0) {
                    expect(wf[0].start).toBe(0);
                }
                for (let i = 0; i < wf.length; i++) {
                    expect(wf[i].end).toBe(
                        wf[i].start + wf[i].new_count + wf[i].recurring_count - wf[i].resolved_count,
                    );
                    if (i > 0) {
                        expect(wf[i].start).toBe(wf[i - 1].end);
                    }
                }
            }),
            { numRuns: 25 },
        );
    });

    it('GET /category-trend has at most one row per (date, category) pair', async () => {
        await fc.assert(
            fc.asyncProperty(arbScenarioSingleUploadPerDate, async (uploads) => {
                installReadEndpointHandler(uploads, []);
                const res = await request(server, 'GET', '/api/compliance/category-trend');
                expect(res.statusCode).toBe(200);

                const seen = new Set();
                for (const row of res.body.categoryTrend) {
                    const key = `${row.report_date}|${row.category}`;
                    expect(seen.has(key)).toBe(false);
                    seen.add(key);
                }
            }),
            { numRuns: 25 },
        );
    });

    it('GET /summary returns null upload when no selectable upload exists, otherwise discloses no siblings', async () => {
        await fc.assert(
            fc.asyncProperty(arbScenarioSingleUploadPerDate, async (uploads) => {
                installReadEndpointHandler(uploads, []);
                const res = await request(server, 'GET', '/api/compliance/summary');
                expect(res.statusCode).toBe(200);

                const selectable = uploads.filter(u =>
                    (u.vertical == null || u.vertical === 'NTS_AEO') && u.summary_json,
                );

                if (selectable.length === 0) {
                    expect(res.body).toEqual({ entries: [], overall_scores: {}, upload: null });
                    return;
                }

                // A primary upload exists. Bug-condition negation holds, so
                // no sibling upload shares its report_date.
                expect(res.body.upload).not.toBeNull();
                const primaryDate = res.body.upload.report_date;
                const siblings = uploads.filter(u =>
                    u.report_date === primaryDate && u.id !== res.body.upload.id,
                );
                expect(siblings).toHaveLength(0);
            }),
            { numRuns: 25 },
        );
    });
});

