package com.example.support;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.FileInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Owned post-run test-management publisher (L3git). Reads the JUnit XML that Surefire already writes
 * ({@code target/surefire-reports}) and pushes the result to whichever integrations are configured -
 * Slack, Microsoft Teams, TestRail, and Xray. Pure JDK: {@link java.net.http.HttpClient} + the
 * built-in XML parser, so there is NO extra dependency and nothing to trust.
 *
 * <p>It is <b>opt-in and offline-safe</b>: a target runs only when its config keys are present, so a
 * project with no reporting config (or one running air-gapped) does nothing. Config comes from a
 * gitignored {@code reporting.properties} (or {@code -Dl3git.reporting=PATH} / {@code L3GIT_REPORTING})
 * with per-key environment-variable overrides, so CI secrets never live in the repo.
 *
 * <p>Run it after {@code mvn test} in CI (the generated Jenkinsfile / GitHub Actions do this):
 * <pre>  java -cp target/test-classes com.example.support.TestReporter</pre>
 *
 * Keys: report.name, report.build, report.surefire.dir (default target/surefire-reports),
 * report.slack.webhook, report.teams.webhook,
 * report.testrail.url / .user / .key / .run  (results pushed by @C&lt;id&gt; in the scenario name),
 * report.xray.url / .token  (JUnit XML imported to the Xray endpoint). Generated / shipped by L3git.
 */
public final class TestReporter {

    private TestReporter() { }

    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(15)).build();
    private static final Pattern CASE_ID = Pattern.compile("\\bC(\\d+)\\b");

    static final class Summary {
        int total, failures, errors, skipped;
        final List<String> failed = new ArrayList<>();
        // case id -> passed? (for TestRail); derived from "C123" in the scenario name
        final Map<Integer, Boolean> cases = new LinkedHashMap<>();
        int passed() { return total - failures - errors - skipped; }
        boolean ok() { return failures == 0 && errors == 0; }
    }

    public static void main(String[] args) {
        Properties cfg = loadConfig();
        String dir = get(cfg, "report.surefire.dir", "target/surefire-reports");
        Summary s = readSurefire(new File(dir));
        if (s.total == 0) {
            System.out.println("[TestReporter] no JUnit results under " + dir + " - nothing to publish.");
            return;
        }
        String name = get(cfg, "report.name", "L3git test run");
        String build = get(cfg, "report.build", "");
        System.out.printf("[TestReporter] %s: %d passed, %d failed, %d skipped (of %d).%n",
                name, s.passed(), s.failures + s.errors, s.skipped, s.total);

        int published = 0;
        published += slackTeams(get(cfg, "report.slack.webhook", ""), name, build, s, false);
        published += slackTeams(get(cfg, "report.teams.webhook", ""), name, build, s, true);
        published += testRail(cfg, s);
        published += xray(cfg, dir);
        if (published == 0) {
            System.out.println("[TestReporter] no integrations configured - printed the summary only.");
        }
    }

    // ---- Surefire JUnit XML -> Summary -----------------------------------------------------------

    static Summary readSurefire(File dir) {
        Summary s = new Summary();
        File[] files = dir.listFiles((d, n) -> n.startsWith("TEST-") && n.endsWith(".xml"));
        if (files == null) { return s; }
        for (File f : files) {
            try {
                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
                DocumentBuilder db = dbf.newDocumentBuilder();
                Document doc;
                try (FileInputStream in = new FileInputStream(f)) { doc = db.parse(in); }
                NodeList cases = doc.getElementsByTagName("testcase");
                for (int i = 0; i < cases.getLength(); i++) {
                    Element tc = (Element) cases.item(i);
                    s.total++;
                    String label = tc.getAttribute("name");
                    boolean failed = tc.getElementsByTagName("failure").getLength() > 0;
                    boolean errored = tc.getElementsByTagName("error").getLength() > 0;
                    boolean skip = tc.getElementsByTagName("skipped").getLength() > 0;
                    if (failed) { s.failures++; }
                    if (errored) { s.errors++; }
                    if (skip) { s.skipped++; }
                    if (failed || errored) { s.failed.add(label); }
                    Matcher m = CASE_ID.matcher(label);
                    while (m.find()) {
                        int id = Integer.parseInt(m.group(1));
                        boolean pass = !(failed || errored);
                        s.cases.merge(id, pass, (a, b) -> a && b);
                    }
                }
            } catch (Exception e) {
                System.out.println("[TestReporter] skipped " + f.getName() + ": " + e.getMessage());
            }
        }
        return s;
    }

    // ---- Slack / Teams incoming webhooks ---------------------------------------------------------

    private static int slackTeams(String url, String name, String build, Summary s, boolean teams) {
        if (isBlank(url)) { return 0; }
        String status = s.ok() ? "PASSED" : "FAILED";
        StringBuilder text = new StringBuilder();
        text.append(s.ok() ? ":white_check_mark: " : ":x: ").append(name);
        if (!isBlank(build)) { text.append(" (").append(build).append(")"); }
        text.append(" - ").append(status).append("\\n")
            .append(s.passed()).append(" passed, ").append(s.failures + s.errors)
            .append(" failed, ").append(s.skipped).append(" skipped of ").append(s.total);
        int show = Math.min(s.failed.size(), 10);
        for (int i = 0; i < show; i++) { text.append("\\n- ").append(jsonEscape(s.failed.get(i))); }
        if (s.failed.size() > show) { text.append("\\n- ...and ").append(s.failed.size() - show).append(" more"); }
        // Teams accepts {"text": ...}; Slack accepts {"text": ...} too (both honor a plain text field).
        String body = "{\"text\":\"" + text + "\"}";
        return post(url, body, "application/json", null, teams ? "Teams" : "Slack") ? 1 : 0;
    }

    // ---- TestRail: add_results_for_cases --------------------------------------------------------

    private static int testRail(Properties cfg, Summary s) {
        String base = get(cfg, "report.testrail.url", "");
        String user = get(cfg, "report.testrail.user", "");
        String key = get(cfg, "report.testrail.key", "");
        String run = get(cfg, "report.testrail.run", "");
        if (isBlank(base) || isBlank(user) || isBlank(key) || isBlank(run)) { return 0; }
        if (s.cases.isEmpty()) {
            System.out.println("[TestReporter] TestRail configured but no @C<id> case ids found in scenario names - skipped.");
            return 0;
        }
        StringBuilder results = new StringBuilder("{\"results\":[");
        boolean first = true;
        for (Map.Entry<Integer, Boolean> e : s.cases.entrySet()) {
            if (!first) { results.append(","); }
            first = false;
            // TestRail status ids: 1=passed, 5=failed
            results.append("{\"case_id\":").append(e.getKey())
                   .append(",\"status_id\":").append(e.getValue() ? 1 : 5).append("}");
        }
        results.append("]}");
        String url = base.replaceAll("/+$", "") + "/index.php?/api/v2/add_results_for_cases/" + run;
        String auth = "Basic " + Base64.getEncoder().encodeToString((user + ":" + key).getBytes(StandardCharsets.UTF_8));
        return post(url, results.toString(), "application/json", auth, "TestRail") ? 1 : 0;
    }

    // ---- Xray: import a JUnit XML execution -----------------------------------------------------

    private static int xray(Properties cfg, String dir) {
        String url = get(cfg, "report.xray.url", "");
        String token = get(cfg, "report.xray.token", "");
        if (isBlank(url) || isBlank(token)) { return 0; }
        File[] files = new File(dir).listFiles((d, n) -> n.startsWith("TEST-") && n.endsWith(".xml"));
        if (files == null || files.length == 0) { return 0; }
        try {
            // Concatenate is unnecessary; Xray accepts one <testsuite> file. Send the first (or a merged one).
            byte[] xml = Files.readAllBytes(files[0].toPath());
            String auth = token.toLowerCase().startsWith("bearer ") ? token : ("Bearer " + token);
            return post(url, new String(xml, StandardCharsets.UTF_8), "application/xml", auth, "Xray") ? 1 : 0;
        } catch (Exception e) {
            System.out.println("[TestReporter] Xray import failed: " + e.getMessage());
            return 0;
        }
    }

    // ---- HTTP + helpers -------------------------------------------------------------------------

    private static boolean post(String url, String body, String contentType, String auth, String label) {
        try {
            HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(url))
                    .timeout(Duration.ofSeconds(30))
                    .header("Content-Type", contentType)
                    .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
            if (auth != null) { b.header("Authorization", auth); }
            HttpResponse<String> r = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
            boolean ok = r.statusCode() >= 200 && r.statusCode() < 300;
            System.out.println("[TestReporter] " + label + " -> HTTP " + r.statusCode() + (ok ? " (ok)" : " " + trim(r.body())));
            return ok;
        } catch (Exception e) {
            System.out.println("[TestReporter] " + label + " publish failed: " + e.getMessage());
            return false;
        }
    }

    private static Properties loadConfig() {
        Properties p = new Properties();
        String path = System.getProperty("l3git.reporting",
                System.getenv().getOrDefault("L3GIT_REPORTING", "reporting.properties"));
        File f = new File(path);
        if (f.isFile()) {
            try (FileInputStream in = new FileInputStream(f)) { p.load(in); }
            catch (Exception e) { System.out.println("[TestReporter] could not read " + path + ": " + e.getMessage()); }
        }
        return p;
    }

    // env override: report.slack.webhook -> REPORT_SLACK_WEBHOOK
    private static String get(Properties cfg, String key, String def) {
        String env = System.getenv(key.toUpperCase().replace('.', '_'));
        if (!isBlank(env)) { return env.trim(); }
        String v = cfg.getProperty(key);
        return isBlank(v) ? def : v.trim();
    }

    private static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); }
    private static String trim(String s) { return s == null ? "" : (s.length() > 200 ? s.substring(0, 200) : s); }
    private static String jsonEscape(String s) {
        return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", " ").replace("\r", " ");
    }
}
