/**
 * Property-Based Tests: VCL Compliance Reporting
 *
 * Feature: vcl-compliance-reporting
 *
 * Tests the pure helper functions used for VCL compliance reporting computations.
 *
 * Validates: Requirements 2.4, 2.5, 3.2, 3.3, 5.2, 5.3, 6.1, 6.3, 7.5, 8.2, 8.3, 8.4, 8.7, 9.2, 9.3, 9.6
 */

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

// Mock db pool before importing anything (avoids DATABASE_URL requirement)
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(),
    })),
}));

// Mock dependencies that the route module imports
jest.mock('../helpers/auditLog', () => jest.fn());
jest.mock('../helpers/ivantiApi', () => ({
    ivantiFormPost: jest.fn(),
    ivantiPost: jest.fn(),
}));

const {
    truncateText,
    validateRemediationPlan,
    computeVCLStats,
    formatPct,
    categorizeNonCompliant,
    rankHeavyHitters,
    computeForecastBurndown,
    matchByHostname,
    computeBulkDiff,
    mapColumnHeaders,
    isValidDateString,
} = require('../helpers/vclHelpers');

// --- Generators ---

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

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 }), // 1-28 always valid
}).map(({ year, month, day }) =>
    `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
);

const complianceItemArb = fc.record({
    hostname: hostnameArb,
    is_compliant: fc.boolean(),
    in_scope: fc.constant(true),
});

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

const verticalArb = fc.record({
    vertical: fc.string({ minLength: 1, maxLength: 20 }),
    team: fc.string({ minLength: 1, maxLength: 20 }),
    non_compliant: fc.integer({ min: 0, max: 1000 }),
});

// --- Property 2: Text Truncation ---

describe('Feature: vcl-compliance-reporting, Property 2: Text Truncation', () => {
    /**
     * For any string, truncateText(text, 80) should return the original string if its
     * length is <= 80, or the first 80 characters followed by "…" if its length exceeds 80.
     *
     * **Validates: Requirements 2.4**
     */
    it('returns original for short strings, truncated + ellipsis for long strings', () => {
        fc.assert(
            fc.property(
                fc.string({ minLength: 0, maxLength: 200 }),
                fc.integer({ min: 1, max: 100 }),
                (text, maxLen) => {
                    const result = truncateText(text, maxLen);
                    if (text.length <= maxLen) {
                        expect(result).toBe(text);
                    } else {
                        expect(result).toBe(text.slice(0, maxLen) + '\u2026');
                        expect(result.length).toBe(maxLen + 1);
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 3: Remediation Plan Length Validation ---

describe('Feature: vcl-compliance-reporting, Property 3: Remediation Plan Length Validation', () => {
    /**
     * For any string, validateRemediationPlan(text) should return valid if and only if
     * the string length is <= 2000 characters.
     *
     * **Validates: Requirements 2.5, 9.4**
     */
    it('accepts strings <= 2000 chars, rejects longer', () => {
        fc.assert(
            fc.property(
                fc.string({ minLength: 1, maxLength: 3000 }),
                (text) => {
                    const result = validateRemediationPlan(text);
                    if (text.length <= 2000) {
                        expect(result.valid).toBe(true);
                    } else {
                        expect(result.valid).toBe(false);
                        expect(result.error).toBeDefined();
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 4: Summary Statistics Computation Invariants ---

describe('Feature: vcl-compliance-reporting, Property 4: Summary Statistics Computation Invariants', () => {
    /**
     * For any set of compliance items, computeVCLStats produces correct arithmetic:
     * non_compliant + compliant = in_scope, and correct percentage.
     *
     * **Validates: Requirements 3.2, 7.3**
     */
    it('non_compliant + compliant = in_scope, correct percentage', () => {
        fc.assert(
            fc.property(
                fc.array(complianceItemArb, { minLength: 0, maxLength: 50 }),
                fc.integer({ min: 0, max: 100 }),
                (items, targetPct) => {
                    const stats = computeVCLStats(items, targetPct);

                    // in_scope items are those with in_scope === true
                    const in_scope = items.filter(i => i.in_scope).length;
                    const compliant = items.filter(i => i.is_compliant).length;

                    expect(stats.non_compliant + stats.compliant).toBe(stats.in_scope);
                    expect(stats.in_scope).toBe(in_scope);
                    expect(stats.compliant).toBe(compliant);

                    if (in_scope > 0) {
                        expect(stats.compliance_pct).toBe(Math.round((compliant / in_scope) * 100));
                    } else {
                        expect(stats.compliance_pct).toBe(0);
                    }

                    expect(stats.target_pct).toBe(targetPct);
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 5: Percentage Formatting ---

describe('Feature: vcl-compliance-reporting, Property 5: Percentage Formatting', () => {
    /**
     * For any decimal number between 0 and 1, formatPct produces a string matching /^\d{1,3}%$/.
     *
     * **Validates: Requirements 3.3**
     */
    it('produces correct percentage string matching /^\\d{1,3}%$/', () => {
        fc.assert(
            fc.property(
                fc.double({ min: 0, max: 1, noNaN: true }),
                (decimal) => {
                    const result = formatPct(decimal);
                    expect(result).toMatch(/^\d{1,3}%$/);
                    expect(result).toBe(Math.round(decimal * 100) + '%');
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 6: Non-Compliant Device Categorization Partition ---

describe('Feature: vcl-compliance-reporting, Property 6: Non-Compliant Device Categorization Partition', () => {
    /**
     * For any array of non-compliant device objects, categorizeNonCompliant produces
     * two groups (blocked, in_progress) where blocked.count + in_progress.count = items.length.
     *
     * **Validates: Requirements 5.2, 5.3**
     */
    it('two groups sum to total', () => {
        fc.assert(
            fc.property(
                fc.array(nonCompliantItemArb, { minLength: 0, maxLength: 50 }),
                (items) => {
                    const result = categorizeNonCompliant(items);

                    expect(result.blocked.count + result.in_progress.count).toBe(items.length);

                    if (items.length > 0) {
                        expect(result.blocked.pct).toBe(Math.round((result.blocked.count / items.length) * 100));
                        expect(result.in_progress.pct).toBe(Math.round((result.in_progress.count / items.length) * 100));
                    } else {
                        expect(result.blocked.pct).toBe(0);
                        expect(result.in_progress.pct).toBe(0);
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 7: Heavy Hitters Descending Sort ---

describe('Feature: vcl-compliance-reporting, Property 7: Heavy Hitters Descending Sort', () => {
    /**
     * For any array of vertical objects, rankHeavyHitters returns the array sorted
     * in non-increasing order by non_compliant.
     *
     * **Validates: Requirements 6.1, 6.3**
     */
    it('sorted non-increasing by non_compliant', () => {
        fc.assert(
            fc.property(
                fc.array(verticalArb, { minLength: 0, maxLength: 30 }),
                (verticals) => {
                    const result = rankHeavyHitters(verticals);

                    expect(result.length).toBe(verticals.length);

                    for (let i = 1; i < result.length; i++) {
                        expect(result[i - 1].non_compliant).toBeGreaterThanOrEqual(result[i].non_compliant);
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 8: Forecasted Burndown Projection ---

describe('Feature: vcl-compliance-reporting, Property 8: Forecasted Burndown Projection', () => {
    /**
     * For any set of non-compliant devices with resolution_date values,
     * computeForecastBurndown produces monthly buckets where the sum of all
     * monthly forecast counts equals the number of items with non-null resolution_dates.
     *
     * **Validates: Requirements 7.5**
     */
    it('bucket sum = count of items with non-null resolution_dates', () => {
        fc.assert(
            fc.property(
                fc.array(nonCompliantItemArb, { minLength: 0, maxLength: 50 }),
                (items) => {
                    const buckets = computeForecastBurndown(items);
                    const bucketSum = Object.values(buckets).reduce((sum, count) => sum + count, 0);
                    const itemsWithDate = items.filter(i => i.resolution_date != null).length;

                    expect(bucketSum).toBe(itemsWithDate);
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 9: Hostname Matching with Unmatched Flagging ---

describe('Feature: vcl-compliance-reporting, Property 9: Hostname Matching with Unmatched Flagging', () => {
    /**
     * For any array of uploaded rows and a set of existing hostnames,
     * matchByHostname produces matched + unmatched = total, and matched hostnames
     * all exist in the set.
     *
     * **Validates: Requirements 8.2, 8.7**
     */
    it('matched + unmatched = total, matched hostnames in set', () => {
        fc.assert(
            fc.property(
                fc.array(fc.record({ hostname: hostnameArb }), { minLength: 0, maxLength: 30 }),
                fc.array(hostnameArb, { minLength: 0, maxLength: 20 }),
                (rows, existingList) => {
                    const existingSet = new Set(existingList);
                    const { matched, unmatched } = matchByHostname(rows, existingSet);

                    expect(matched.length + unmatched.length).toBe(rows.length);

                    for (const row of matched) {
                        expect(existingSet.has(row.hostname)).toBe(true);
                    }

                    for (const row of unmatched) {
                        expect(existingSet.has(row.hostname)).toBe(false);
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 10: Bulk Diff Change Detection ---

describe('Feature: vcl-compliance-reporting, Property 10: Bulk Diff Change Detection', () => {
    /**
     * For any array of matched row pairs, computeBulkDiff flags a row as "changed"
     * if and only if at least one field value differs.
     *
     * **Validates: Requirements 8.3, 8.4**
     */
    it('changed iff at least one field differs', () => {
        const fieldValueArb = fc.oneof(fc.constant(null), fc.string({ minLength: 1, maxLength: 20 }));

        // When uploaded values match current data exactly, status should be 'unchanged'
        fc.assert(
            fc.property(
                fc.array(hostnameArb, { minLength: 1, maxLength: 20 }).chain(hostnames => {
                    // Ensure unique hostnames to avoid map overwrite issues
                    const uniqueHostnames = [...new Set(hostnames)];
                    return fc.tuple(
                        ...uniqueHostnames.map(h =>
                            fc.record({
                                hostname: fc.constant(h),
                                resolution_date: fieldValueArb,
                                remediation_plan: fieldValueArb,
                                notes: fieldValueArb,
                            })
                        )
                    );
                }),
                (matchedRows) => {
                    // Build currentData with same values as uploaded
                    const currentData = new Map();
                    for (const row of matchedRows) {
                        currentData.set(row.hostname, {
                            resolution_date: row.resolution_date,
                            remediation_plan: row.remediation_plan,
                            notes: row.notes,
                        });
                    }

                    const results = computeBulkDiff(matchedRows, currentData);
                    for (const r of results) {
                        expect(r.status).toBe('unchanged');
                    }
                }
            ),
            { numRuns: 50 }
        );

        // When at least one field differs, status should be 'changed'
        fc.assert(
            fc.property(
                hostnameArb,
                fc.string({ minLength: 1, maxLength: 20 }),
                fc.string({ minLength: 1, maxLength: 20 }),
                (hostname, oldVal, newVal) => {
                    fc.pre(oldVal !== newVal);

                    const matchedRows = [{ hostname, resolution_date: newVal }];
                    const currentData = new Map();
                    currentData.set(hostname, { resolution_date: oldVal, remediation_plan: null, notes: null });

                    const results = computeBulkDiff(matchedRows, currentData);
                    expect(results[0].status).toBe('changed');
                }
            ),
            { numRuns: 50 }
        );
    });
});

// --- Property 11: Column Header Mapping ---

describe('Feature: vcl-compliance-reporting, Property 11: Column Header Mapping', () => {
    /**
     * mapColumnHeaders correctly identifies known columns case-insensitively.
     *
     * **Validates: Requirements 9.2**
     */
    it('identifies known columns case-insensitively', () => {
        const knownHeaders = ['Hostname', 'Resolution Date', 'Remediation Plan', 'Notes',
            'hostname', 'resolution_date', 'remediation_plan', 'notes',
            'HOSTNAME', 'RESOLUTION DATE', 'REMEDIATION PLAN', 'NOTES'];

        const caseVariantArb = fc.constantFrom(...knownHeaders);
        const unknownHeaderArb = fc.stringMatching(/^[a-z]{5,10}$/).filter(
            s => !['hostname', 'notes'].includes(s.toLowerCase())
        );

        fc.assert(
            fc.property(
                fc.array(fc.oneof(caseVariantArb, unknownHeaderArb), { minLength: 1, maxLength: 10 }),
                (headers) => {
                    const mapping = mapColumnHeaders(headers);

                    // Every mapped key should be a known field
                    const validKeys = new Set(['hostname', 'resolution_date', 'remediation_plan', 'notes']);
                    for (const key of Object.keys(mapping)) {
                        expect(validKeys.has(key)).toBe(true);
                    }

                    // Check that known headers are mapped correctly
                    for (let i = 0; i < headers.length; i++) {
                        const normalized = headers[i].trim().toLowerCase();
                        if (normalized === 'hostname') {
                            expect(mapping.hostname).toBeDefined();
                        }
                        if (normalized === 'resolution date' || normalized === 'resolution_date') {
                            expect(mapping.resolution_date).toBeDefined();
                        }
                        if (normalized === 'remediation plan' || normalized === 'remediation_plan') {
                            expect(mapping.remediation_plan).toBeDefined();
                        }
                        if (normalized === 'notes') {
                            expect(mapping.notes).toBeDefined();
                        }
                    }
                }
            ),
            { numRuns: 100 }
        );
    });
});

// --- Property 12: Date String Validation ---

describe('Feature: vcl-compliance-reporting, Property 12: Date String Validation', () => {
    /**
     * isValidDateString rejects invalid calendar dates and non-date strings.
     * Returns true only for valid YYYY-MM-DD dates.
     *
     * **Validates: Requirements 9.3**
     */
    it('rejects invalid dates and non-date strings', () => {
        // Valid dates should return true
        fc.assert(
            fc.property(validDateArb, (dateStr) => {
                expect(isValidDateString(dateStr)).toBe(true);
            }),
            { numRuns: 50 }
        );

        // Invalid dates should return false
        const invalidDateArb = fc.oneof(
            fc.constant(null),
            fc.constant(''),
            fc.constant('not-a-date'),
            fc.constant('2026-02-30'),
            fc.constant('2026-13-01'),
            fc.constant('2026-00-15'),
            fc.constant('abcd-ef-gh'),
            fc.integer().map(n => String(n)),
            fc.string({ minLength: 1, maxLength: 5 }),
        );

        fc.assert(
            fc.property(invalidDateArb, (val) => {
                expect(isValidDateString(val)).toBe(false);
            }),
            { numRuns: 50 }
        );
    });
});

// --- Property 13: Row Count Arithmetic Invariant ---

describe('Feature: vcl-compliance-reporting, Property 13: Row Count Arithmetic (matched + unmatched = total)', () => {
    /**
     * For any bulk upload, matched + unmatched = total input rows.
     *
     * **Validates: Requirements 9.6**
     */
    it('matched + unmatched = total invariant holds', () => {
        fc.assert(
            fc.property(
                fc.array(fc.record({ hostname: hostnameArb }), { minLength: 0, maxLength: 50 }),
                fc.array(hostnameArb, { minLength: 0, maxLength: 30 }),
                (rows, existingList) => {
                    const existingSet = new Set(existingList);
                    const { matched, unmatched } = matchByHostname(rows, existingSet);

                    // Core invariant: matched + unmatched = total
                    expect(matched.length + unmatched.length).toBe(rows.length);
                }
            ),
            { numRuns: 100 }
        );
    });
});
