package com.example.support;

import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * A lightweight, owned accessibility check (L3git). Injects the bundled axe-core engine into the
 * page under test, runs it against the live DOM, and returns evidence-based WCAG violations. Nothing
 * is fetched at test time - {@code axe.min.js} ships on the classpath ({@code /axe/axe.min.js}), so
 * the check runs fully offline once the framework's own dependencies are present.
 *
 * <p>Report-only by default: {@link #check} returns the findings and {@link #report} prints them.
 * An OPTIONAL build-fail gate ({@link #assertClean}) fails the step when a violation at or above a
 * chosen impact threshold exists - the same shift-left pattern as {@link SecurityScan}. axe-core is
 * vendored unmodified (Deque Systems, MPL-2.0). Generated / shipped by L3git.
 */
public final class Accessibility {

    private Accessibility() { }

    // Impact ranking so a threshold like "serious" includes "critical" too. Anything unknown/null
    // is treated as "minor" (rank 1); "any" (rank 0) keeps every violation.
    private static int rank(String impact) {
        if (impact == null) { return 1; }
        switch (impact.toLowerCase()) {
            case "any":      return 0;
            case "minor":    return 1;
            case "moderate": return 2;
            case "serious":  return 3;
            case "critical": return 4;
            default:         return 1;
        }
    }

    /** Cached axe-core source (read once from the classpath). */
    private static volatile String axeSource;

    private static String axeSource() {
        String s = axeSource;
        if (s != null) { return s; }
        try (InputStream in = Accessibility.class.getResourceAsStream("/axe/axe.min.js")) {
            if (in == null) {
                throw new IllegalStateException(
                        "axe.min.js not found on the classpath at /axe/axe.min.js - the framework resources were not copied.");
            }
            s = new String(in.readAllBytes(), StandardCharsets.UTF_8);
            axeSource = s;
            return s;
        } catch (IOException e) {
            throw new RuntimeException("Failed to read bundled axe.min.js", e);
        }
    }

    /**
     * Scan the current page and return violations at or above {@code minImpact}.
     *
     * @param driver    the live browser (self-healing tests pass {@code driver.raw()})
     * @param minImpact any | minor | moderate | serious | critical
     * @param scopeCss  a CSS selector to limit the scan to (blank/null = the whole page)
     * @param wcagTags  optional WCAG tag filter, e.g. "wcag2a", "wcag2aa", "wcag21aa" (blank = all rules)
     */
    @SuppressWarnings("unchecked")
    public static List<A11yViolation> check(WebDriver driver, String minImpact, String scopeCss, String wcagTags) {
        JavascriptExecutor js = (JavascriptExecutor) driver;

        // axe.run() is async; give the async script room and inject the engine once per page.
        driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(60));
        Boolean present = (Boolean) js.executeScript("return !!window.axe;");
        if (present == null || !present) {
            js.executeScript(axeSource());
        }

        Object ctx = (scopeCss != null && !scopeCss.trim().isEmpty()) ? scopeCss.trim() : null;
        Object opts = null;
        if (wcagTags != null && !wcagTags.trim().isEmpty()) {
            List<String> tags = new ArrayList<>();
            for (String t : wcagTags.split("[,\\s]+")) { if (!t.isEmpty()) { tags.add(t); } }
            Map<String, Object> runOnly = new java.util.LinkedHashMap<>();
            runOnly.put("type", "tag");
            runOnly.put("values", tags);
            Map<String, Object> o = new java.util.LinkedHashMap<>();
            o.put("runOnly", runOnly);
            opts = o;
        }

        Object raw = js.executeAsyncScript(
                "var done = arguments[arguments.length - 1];"
                        + "var ctx = arguments[0]; var opts = arguments[1] || {};"
                        + "try {"
                        + "  axe.run(ctx || document, opts)"
                        + "     .then(function(r){ done(r); })"
                        + "     .catch(function(e){ done({ __error: String(e) }); });"
                        + "} catch (e) { done({ __error: String(e) }); }",
                ctx, opts);

        Map<String, Object> result = (Map<String, Object>) raw;
        if (result != null && result.get("__error") != null) {
            throw new RuntimeException("axe-core failed to run: " + result.get("__error"));
        }

        List<A11yViolation> out = new ArrayList<>();
        if (result == null) { return out; }
        Object vObj = result.get("violations");
        if (!(vObj instanceof List)) { return out; }

        int threshold = rank(minImpact);
        for (Object item : (List<Object>) vObj) {
            if (!(item instanceof Map)) { continue; }
            Map<String, Object> v = (Map<String, Object>) item;
            String impact = str(v.get("impact"));
            if (rank(impact) < threshold) { continue; }

            String id = str(v.get("id"));
            String help = str(v.get("help"));
            String helpUrl = str(v.get("helpUrl"));

            int nodeCount = 0;
            String sampleTarget = "";
            String sampleHtml = "";
            Object nObj = v.get("nodes");
            if (nObj instanceof List) {
                List<Object> nodes = (List<Object>) nObj;
                nodeCount = nodes.size();
                if (!nodes.isEmpty() && nodes.get(0) instanceof Map) {
                    Map<String, Object> n0 = (Map<String, Object>) nodes.get(0);
                    Object tgt = n0.get("target");
                    if (tgt instanceof List && !((List<Object>) tgt).isEmpty()) {
                        sampleTarget = str(((List<Object>) tgt).get(0));
                    }
                    sampleHtml = str(n0.get("html"));
                    if (sampleHtml.length() > 160) { sampleHtml = sampleHtml.substring(0, 157) + "..."; }
                }
            }
            out.add(new A11yViolation(id, impact, help, helpUrl, nodeCount, sampleTarget, sampleHtml));
        }
        return out;
    }

    /** Human-readable report of the violations (printed to the console during a run). */
    public static String report(List<A11yViolation> violations) {
        StringBuilder sb = new StringBuilder();
        sb.append("=== Accessibility scan (axe-core) ===\n");
        if (violations == null || violations.isEmpty()) {
            sb.append("No accessibility violations found at the requested impact level.\n");
            return sb.toString();
        }
        sb.append(violations.size()).append(violations.size() == 1 ? " rule violated:\n" : " rules violated:\n");
        for (A11yViolation v : violations) {
            sb.append("  ").append(v).append('\n');
            if (!v.sampleTarget.isEmpty()) { sb.append("      at: ").append(v.sampleTarget).append('\n'); }
            if (!v.sampleHtml.isEmpty())   { sb.append("      html: ").append(v.sampleHtml).append('\n'); }
            if (!v.helpUrl.isEmpty())      { sb.append("      more: ").append(v.helpUrl).append('\n'); }
        }
        return sb.toString();
    }

    /**
     * Convenience for generated steps: scan, print the report, and fail the step (AssertionError)
     * if any violation at or above {@code minImpact} was found.
     */
    public static void assertClean(WebDriver driver, String minImpact, String scopeCss, String wcagTags) {
        List<A11yViolation> violations = check(driver, minImpact, scopeCss, wcagTags);
        System.out.println(report(violations));
        if (!violations.isEmpty()) {
            String where = (scopeCss != null && !scopeCss.trim().isEmpty()) ? (" in " + scopeCss.trim()) : "";
            throw new AssertionError(violations.size() + " accessibility violation(s) at "
                    + (minImpact == null || minImpact.isEmpty() ? "any" : minImpact)
                    + " impact or higher" + where + " - see the report above.");
        }
    }

    private static String str(Object o) { return o == null ? "" : String.valueOf(o); }
}
