/**
 * Property-Based Tests: VCL Aggregated Burndown
 *
 * Feature: vcl-aggregated-burndown
 *
 * Tests the pure helper functions `deduplicateByHostname` and `computeAggregatedBurndown`
 * from `backend/helpers/vclHelpers.js`.
 *
 * Validates: Requirements 1.5, 1.6, 1.7, 2.2, 2.3, 2.4, 2.5, 4.1, 4.2, 4.3, 4.4, 5.1, 5.2, 5.4
 */

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

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

jest.mock('../helpers/auditLog', () => jest.fn());
jest.mock('../helpers/ivantiApi', () => ({
    ivantiFormPost: jest.fn(),
    ivantiPost: jest.fn(),
}));

const {
    deduplicateByHostname,
    computeAggregatedBurndown,
} = require('../helpers/vclHelpers');

// --- Generators ---

const hostnameArb = fc.stringMatching(/^[a-zA-Z0-9._-]+$/, { minLength: 1, maxLength: 20 });

const validDateArb = fc.record({
    year: fc.integer({ min: 2020, max: 2030 }),
    month: fc.integer({ min: 1, max: 12 }),
    day: fc.integer({ min: 1, max: 28 }),
}).map(({ year, month, day }) =>
    `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
);

const verticalCodeArb = fc.constantFrom('NTS_AEO', 'SDIT_CISO', 'TSI', 'SR', 'AllOthers');

const deviceArb = fc.record({
    hostname: hostnameArb,
    resolution_date: fc.oneof(fc.constant(null), validDateArb),
    vertical: verticalCodeArb,
});

// Generator for items that may have duplicate hostnames (for deduplication testing)
const duplicateItemsArb = fc.array(
    fc.record({
        hostname: fc.constantFrom('srv-001', 'srv-002', 'srv-003', 'srv-004', 'srv-005'),
        resolution_date: fc.oneof(fc.constant(null), validDateArb),
        vertical: verticalCodeArb,
    }),
    { minLength: 0, maxLength: 30 }
);

// --- Property 1: Partition Invariant ---

describe('Feature: vcl-aggregated-burndown, Property 1: Partition Invariant', () => {
    /**
     * For any array of device objects passed to computeAggregatedBurndown,
     * blockers + with_dates = total.
     *
     * **Validates: Requirements 2.2**
     */
    it('blockers + with_dates = total for any input', () => {
        fc.assert(
            fc.property(
                fc.array(deviceArb, { minLength: 0, maxLength: 50 }),
                (devices) => {
                    const result = computeAggregatedBurndown(devices);
                    expect(result.blockers + result.with_dates).toBe(result.total);
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 2: Monthly Bucket Conservation ---

describe('Feature: vcl-aggregated-burndown, Property 2: Monthly Bucket Conservation', () => {
    /**
     * For any array of device objects, the sum of all values in monthly
     * must equal with_dates.
     *
     * **Validates: Requirements 2.3, 1.5**
     */
    it('sum of monthly values = with_dates', () => {
        fc.assert(
            fc.property(
                fc.array(deviceArb, { minLength: 0, maxLength: 50 }),
                (devices) => {
                    const result = computeAggregatedBurndown(devices);
                    const monthlySum = Object.values(result.monthly).reduce((s, v) => s + v, 0);
                    expect(monthlySum).toBe(result.with_dates);
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 3: Chronological Monthly Ordering ---

describe('Feature: vcl-aggregated-burndown, Property 3: Chronological Monthly Ordering', () => {
    /**
     * For any array of device objects, the keys of monthly must be in
     * ascending chronological order (lexicographic sort of YYYY-MM strings).
     *
     * **Validates: Requirements 2.4**
     */
    it('monthly keys are in ascending chronological order', () => {
        fc.assert(
            fc.property(
                fc.array(deviceArb, { minLength: 0, maxLength: 50 }),
                (devices) => {
                    const result = computeAggregatedBurndown(devices);
                    const keys = Object.keys(result.monthly);
                    for (let i = 1; i < keys.length; i++) {
                        expect(keys[i - 1] < keys[i]).toBe(true);
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 4: Cumulative Projection Consistency ---

describe('Feature: vcl-aggregated-burndown, Property 4: Cumulative Projection Consistency', () => {
    /**
     * For any array of device objects, projection[month].remaining =
     * total - (cumulative sum of monthly[m] for all m <= month).
     *
     * **Validates: Requirements 2.5**
     */
    it('projection remaining = total - cumulative remediated', () => {
        fc.assert(
            fc.property(
                fc.array(deviceArb, { minLength: 0, maxLength: 50 }),
                (devices) => {
                    const result = computeAggregatedBurndown(devices);
                    const months = Object.keys(result.monthly);
                    let cumulative = 0;
                    for (const month of months) {
                        cumulative += result.monthly[month];
                        expect(result.projection[month].remediated).toBe(result.monthly[month]);
                        expect(result.projection[month].remaining).toBe(result.total - cumulative);
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 5: Projected Clear Date Logic ---

describe('Feature: vcl-aggregated-burndown, Property 5: Projected Clear Date Logic', () => {
    /**
     * If blockers > 0, projected_clear_date must be null.
     * If blockers = 0 and with_dates > 0, projected_clear_date must equal the last month key.
     *
     * **Validates: Requirements 1.7**
     */
    it('null when blockers > 0, last month key when blockers = 0 and with_dates > 0', () => {
        fc.assert(
            fc.property(
                fc.array(deviceArb, { minLength: 0, maxLength: 50 }),
                (devices) => {
                    const result = computeAggregatedBurndown(devices);
                    if (result.blockers > 0) {
                        expect(result.projected_clear_date).toBeNull();
                    } else if (result.with_dates > 0) {
                        const months = Object.keys(result.monthly);
                        expect(result.projected_clear_date).toBe(months[months.length - 1]);
                    } else {
                        // total = 0 case
                        expect(result.projected_clear_date).toBeNull();
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 6: Hostname Deduplication with Earliest Date ---

describe('Feature: vcl-aggregated-burndown, Property 6: Hostname Deduplication with Earliest Date', () => {
    /**
     * For any array of items where the same hostname appears multiple times,
     * deduplicateByHostname produces exactly one entry per unique hostname,
     * and that entry's resolution_date is the earliest non-null date (or null if all null).
     *
     * **Validates: Requirements 1.6**
     */
    it('one entry per hostname with earliest non-null date', () => {
        fc.assert(
            fc.property(
                duplicateItemsArb,
                (items) => {
                    const result = deduplicateByHostname(items);

                    // One entry per unique hostname
                    const uniqueHostnames = new Set(items.map(i => i.hostname));
                    expect(result.length).toBe(uniqueHostnames.size);

                    // Each result hostname appears exactly once
                    const resultHostnames = result.map(r => r.hostname);
                    expect(new Set(resultHostnames).size).toBe(result.length);

                    // For each hostname, verify the date is the earliest non-null
                    for (const entry of result) {
                        const allForHost = items.filter(i => i.hostname === entry.hostname);
                        const nonNullDates = allForHost
                            .map(i => i.resolution_date)
                            .filter(d => d != null);

                        if (nonNullDates.length === 0) {
                            expect(entry.resolution_date).toBeNull();
                        } else {
                            const earliest = nonNullDates.sort()[0];
                            expect(entry.resolution_date).toBe(earliest);
                        }
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 7: Aggregation Consistency with Per-Vertical Computation ---

describe('Feature: vcl-aggregated-burndown, Property 7: Aggregation Consistency with Per-Vertical Computation', () => {
    /**
     * Aggregated total = sum of per-vertical totals.
     * Aggregated blockers = sum of per-vertical blockers.
     * Aggregated with_dates = sum of per-vertical with_dates.
     *
     * **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
     */
    it('aggregated totals = sum of per-vertical totals', () => {
        fc.assert(
            fc.property(
                fc.array(deviceArb, { minLength: 0, maxLength: 50 }),
                (devices) => {
                    const result = computeAggregatedBurndown(devices);

                    const sumTotal = result.by_vertical.reduce((s, v) => s + v.total, 0);
                    const sumBlockers = result.by_vertical.reduce((s, v) => s + v.blockers, 0);
                    const sumWithDates = result.by_vertical.reduce((s, v) => s + v.with_dates, 0);

                    expect(sumTotal).toBe(result.total);
                    expect(sumBlockers).toBe(result.blockers);
                    expect(sumWithDates).toBe(result.with_dates);
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 8: By-Vertical Sorting and Filtering ---

describe('Feature: vcl-aggregated-burndown, Property 8: By-Vertical Sorting and Filtering', () => {
    /**
     * by_vertical is sorted descending by total, contains no zero-total entries,
     * and the sum of all by_vertical[i].total equals the overall total.
     *
     * **Validates: Requirements 5.1, 5.2, 5.4**
     */
    it('sorted descending by total, no zero entries, sum = total', () => {
        fc.assert(
            fc.property(
                fc.array(deviceArb, { minLength: 0, maxLength: 50 }),
                (devices) => {
                    const result = computeAggregatedBurndown(devices);

                    // Sorted descending by total
                    for (let i = 1; i < result.by_vertical.length; i++) {
                        expect(result.by_vertical[i - 1].total).toBeGreaterThanOrEqual(result.by_vertical[i].total);
                    }

                    // No zero-total entries
                    for (const v of result.by_vertical) {
                        expect(v.total).toBeGreaterThan(0);
                    }

                    // Sum = overall total
                    const sum = result.by_vertical.reduce((s, v) => s + v.total, 0);
                    expect(sum).toBe(result.total);
                }
            ),
            { numRuns: 100 }
        );
    });
});
