/**
 * axe-scan.js
 * WCAG accessibility scanner using Puppeteer + @axe-core/puppeteer.
 * Accepts a URL as the first argument, outputs structured JSON to stdout.
 * All diagnostic/error messages go to stderr so stdout stays clean JSON.
 *
 * Usage:
 *   node axe-scan.js https://example.com
 *   node axe-scan.js https://example.com --timeout=30000
 *   node axe-scan.js https://example.com --level=AA
 *
 * Exit codes:
 *   0 — scan completed (pass or fail — check JSON status field)
 *   1 — fatal error (bad arguments, browser crash, etc.)
 */

'use strict';

const puppeteer   = require('puppeteer');
const { AxePuppeteer } = require('@axe-core/puppeteer');

// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);

if (args.length === 0) {
    process.stderr.write('[ERROR] No URL provided. Usage: node axe-scan.js <url>\n');
    process.exit(1);
}

let inputUrl  = null;
let timeout   = 30000;
let wcagLevel = 'AA'; // default WCAG level to test against

for (const arg of args) {
    if (arg.startsWith('--timeout=')) {
        timeout = parseInt(arg.split('=')[1], 10) || 30000;
    } else if (arg.startsWith('--level=')) {
        wcagLevel = arg.split('=')[1].toUpperCase();
    } else if (!arg.startsWith('--')) {
        inputUrl = arg;
    }
}

if (!inputUrl) {
    process.stderr.write('[ERROR] No URL argument found.\n');
    process.exit(1);
}

// ---------------------------------------------------------------------------
// Normalise URL — ensure it has a protocol
// ---------------------------------------------------------------------------
function normaliseUrl(raw) {
    if (/^https?:\/\//i.test(raw)) return raw;
    return 'https://' + raw;
}

// ---------------------------------------------------------------------------
// Map axe WCAG tags to highest passing compliance level
// ---------------------------------------------------------------------------
function resolveComplianceLevel(violations, incomplete) {
    // Axe tags violations with wcag2a, wcag2aa, wcag2aaa, wcag21a, wcag21aa etc.
    // If there are no violations of a given level, the page passes at that level.
    const hasViolationAtLevel = (level) => {
        const tag = 'wcag2' + level.toLowerCase();
        const tag21 = 'wcag21' + level.toLowerCase();
        const tag22 = 'wcag22' + level.toLowerCase();
        return violations.some(v =>
            v.tags && (v.tags.includes(tag) || v.tags.includes(tag21) || v.tags.includes(tag22))
        );
    };

    if (!hasViolationAtLevel('a'))   return 'AAA'; // passes everything
    if (!hasViolationAtLevel('aa'))  return 'AA';
    if (!hasViolationAtLevel('aaa')) return 'A';
    return 'none';
}

// ---------------------------------------------------------------------------
// Summarise violations by impact/severity
// ---------------------------------------------------------------------------
function summariseBySeverity(violations) {
    const counts = { critical: 0, serious: 0, moderate: 0, minor: 0 };
    for (const v of violations) {
        const impact = v.impact || 'minor';
        if (counts.hasOwnProperty(impact)) {
            counts[impact]++;
        }
    }
    return counts;
}

// ---------------------------------------------------------------------------
// Determine overall pass/fail/partial status
// ---------------------------------------------------------------------------
function resolveStatus(violations, incomplete) {
    if (violations.length === 0 && incomplete.length === 0) return 'pass';
    if (violations.length === 0 && incomplete.length > 0)   return 'partial';
    return 'fail';
}

// ---------------------------------------------------------------------------
// Slim down violation data for storage — keep essentials only
// ---------------------------------------------------------------------------
function slimViolation(v) {
    return {
        id:          v.id,
        impact:      v.impact,
        description: v.description,
        help:        v.help,
        helpUrl:     v.helpUrl,
        tags:        v.tags,
        nodes:       (v.nodes || []).slice(0, 5).map(n => ({
            html:    n.html,
            target:  n.target,
            message: (n.any || []).concat(n.all || []).map(c => c.message).join('; ')
        }))
    };
}

// ---------------------------------------------------------------------------
// Main scan function
// ---------------------------------------------------------------------------
async function scan(url) {
    let browser = null;
    let finalUrl = url;

    try {
        browser = await puppeteer.launch({
            headless: 'new',
            args: [
                '--no-sandbox',
                '--disable-setuid-sandbox',
                '--disable-dev-shm-usage',
                '--disable-gpu',
            ]
        });

        const page = await browser.newPage();
        await page.setViewport({ width: 1280, height: 900 });
        await page.setUserAgent(
            'Mozilla/5.0 (compatible; WMD-WCAG-Scanner/1.0; +https://domainpunch.com/)'
        );

        // -----------------------------------------------------------------------
        // Navigation — try provided URL, fall back to http:// if https:// fails
        // -----------------------------------------------------------------------
        let navError = null;
        try {
            await page.goto(url, { waitUntil: 'networkidle2', timeout });
            finalUrl = page.url();
        } catch (e) {
            navError = e.message;
            process.stderr.write(`[WARN] Navigation failed for ${url}: ${e.message}\n`);

            // If https failed, retry with http
            if (url.startsWith('https://')) {
                const httpUrl = url.replace(/^https:\/\//i, 'http://');
                process.stderr.write(`[INFO] Retrying with ${httpUrl}\n`);
                try {
                    await page.goto(httpUrl, { waitUntil: 'networkidle2', timeout });
                    finalUrl = page.url();
                    navError = null;
                } catch (e2) {
                    navError = e2.message;
                    process.stderr.write(`[ERROR] Retry also failed: ${e2.message}\n`);
                }
            }
        }

        // If navigation failed entirely, return an error result
        if (navError) {
            await browser.close();
            return {
                url:          url,
                finalUrl:     null,
                scannedAt:    new Date().toISOString(),
                status:       'error',
                wcagLevel:    'none',
                issueCounts:  { critical: 0, serious: 0, moderate: 0, minor: 0 },
                violations:   [],
                incomplete:   [],
                passes:       0,
                error:        navError
            };
        }

        // -----------------------------------------------------------------------
        // Run axe
        // -----------------------------------------------------------------------
        const axeBuilder = new AxePuppeteer(page);

        // Configure WCAG tags to test
        const tagMap = {
            'A':   ['wcag2a',  'wcag21a',  'wcag22a',  'best-practice'],
            'AA':  ['wcag2a',  'wcag21a',  'wcag22a',  'wcag2aa', 'wcag21aa', 'wcag22aa', 'best-practice'],
            'AAA': ['wcag2a',  'wcag21a',  'wcag22a',  'wcag2aa', 'wcag21aa', 'wcag22aa', 'wcag2aaa', 'best-practice'],
        };
        const tags = tagMap[wcagLevel] || tagMap['AA'];
        axeBuilder.withTags(tags);

        const results = await axeBuilder.analyze();

        const violations  = results.violations  || [];
        const incomplete  = results.incomplete  || [];
        const passes      = (results.passes     || []).length;

        await browser.close();

        return {
            url:         url,
            finalUrl:    finalUrl,
            scannedAt:   new Date().toISOString(),
            status:      resolveStatus(violations, incomplete),
            wcagLevel:   resolveComplianceLevel(violations, incomplete),
            issueCounts: summariseBySeverity(violations),
            violations:  violations.map(slimViolation),
            incomplete:  incomplete.map(slimViolation),
            passes:      passes,
            error:       null
        };

    } catch (err) {
        if (browser) {
            try { await browser.close(); } catch (_) {}
        }
        throw err;
    }
}

// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
(async () => {
    const url = normaliseUrl(inputUrl);
    process.stderr.write(`[INFO] Scanning: ${url} (WCAG level: ${wcagLevel}, timeout: ${timeout}ms)\n`);

    try {
        const result = await scan(url);
        // Output clean JSON to stdout — this is what PHP captures
        process.stdout.write(JSON.stringify(result, null, 2));
        process.exit(0);
    } catch (err) {
        process.stderr.write(`[FATAL] ${err.message}\n`);
        // Output a valid error JSON so PHP always gets parseable output
        const errorResult = {
            url:         url,
            finalUrl:    null,
            scannedAt:   new Date().toISOString(),
            status:      'error',
            wcagLevel:   'none',
            issueCounts: { critical: 0, serious: 0, moderate: 0, minor: 0 },
            violations:  [],
            incomplete:  [],
            passes:      0,
            error:       err.message
        };
        process.stdout.write(JSON.stringify(errorResult, null, 2));
        process.exit(0); // exit 0 so PHP always gets the JSON — error is in the payload
    }
})();