package com.example.support;

import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;

/**
 * Smart test selection / test-impact analysis (L3git). Each generated test declares the touchpoints
 * it exercises - the URLs it opens and the API paths it calls - in {@code impact-map.properties}
 * ({@code @testTag=keyword,keyword,...}). Given the things that changed (a list of tokens, or the
 * files from a git diff), this prints a Cucumber tag expression selecting ONLY the tests whose
 * touchpoints intersect the change, so CI can run the affected slice instead of the whole suite.
 *
 * <p>Pure JDK ({@link java.util.Properties}) - no dependency. Deliberately a black-box heuristic:
 * matching is case-insensitive substring in both directions (a changed file {@code OrdersApi.java}
 * matches a test that hits {@code /orders}). It errs SAFE - with no change tokens it selects
 * everything, and it prints {@code __NONE__} only when tokens were given and nothing matched, so a
 * CI wrapper can choose to skip the run.
 *
 * <pre>
 *   # run only tests affected by the diff vs origin/main:
 *   TAGS=$(java -cp target/test-classes com.example.support.ImpactSelector --git origin/main)
 *   case "$TAGS" in
 *     __NONE__) echo "no impacted tests - skipping" ;;
 *     "")       mvn test ;;                                   # could not scope - run all
 *     *)        mvn test -Dcucumber.filter.tags="$TAGS" ;;    # run the affected slice
 *   esac
 * </pre>
 * Config: {@code -Dl3git.impactmap=PATH} (default impact-map.properties). Generated / shipped by L3git.
 */
public final class ImpactSelector {

    private ImpactSelector() { }

    public static void main(String[] args) throws Exception {
        List<String> changed = new ArrayList<>();
        for (int i = 0; i < args.length; i++) {
            if ("--git".equals(args[i]) && i + 1 < args.length) {
                changed.addAll(gitChangedFiles(args[++i]));
            } else if ("-".equals(args[i])) {
                changed.addAll(readStdin());
            } else {
                changed.add(args[i]);
            }
        }

        String mapPath = System.getProperty("l3git.impactmap", "impact-map.properties");
        Properties map = new Properties();
        try (FileInputStream in = new FileInputStream(mapPath)) { map.load(in); }
        catch (Exception e) {
            // No map -> cannot scope -> run all (empty filter). Note goes to stderr, not stdout.
            System.err.println("[ImpactSelector] no " + mapPath + " - running all tests.");
            System.out.print("");
            return;
        }

        // Normalize change tokens (split file paths into segments so OrdersApi.java -> ordersapi, java...)
        Set<String> tokens = new LinkedHashSet<>();
        for (String c : changed) {
            for (String part : c.toLowerCase().split("[^a-z0-9]+")) {
                if (part.length() >= 3) { tokens.add(part); }
            }
            String whole = c.toLowerCase().trim();
            if (!whole.isEmpty()) { tokens.add(whole); }
        }

        // Deterministic order for stable output.
        Map<String, String> sorted = new TreeMap<>();
        for (String tag : map.stringPropertyNames()) { sorted.put(tag, map.getProperty(tag)); }

        Set<String> impacted = new LinkedHashSet<>();
        for (Map.Entry<String, String> e : sorted.entrySet()) {
            if (tokens.isEmpty()) { impacted.add(e.getKey()); continue; }   // no change info -> select all
            if (matches(e.getValue(), tokens)) { impacted.add(e.getKey()); }
        }

        if (impacted.isEmpty()) {
            System.out.print(tokens.isEmpty() ? "" : "__NONE__");
            return;
        }
        // Cucumber tag expression: "@a or @b"
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (String tag : impacted) {
            if (!first) { sb.append(" or "); }
            first = false;
            sb.append(tag.startsWith("@") ? tag : ("@" + tag));
        }
        System.out.print(sb.toString());
    }

    private static boolean matches(String keywords, Set<String> tokens) {
        if (keywords == null) { return false; }
        for (String kwRaw : keywords.split(",")) {
            String kw = kwRaw.toLowerCase().trim();
            if (kw.length() < 3) { continue; }
            for (String t : tokens) {
                if (t.contains(kw) || kw.contains(t)) { return true; }
            }
        }
        return false;
    }

    private static List<String> gitChangedFiles(String base) {
        List<String> out = new ArrayList<>();
        try {
            Process p = new ProcessBuilder("git", "diff", "--name-only", base + "...HEAD")
                    .redirectErrorStream(true).start();
            byte[] b = p.getInputStream().readAllBytes();
            p.waitFor();
            for (String line : new String(b, StandardCharsets.UTF_8).split("\\R")) {
                if (!line.trim().isEmpty()) { out.add(line.trim()); }
            }
        } catch (Exception e) {
            System.err.println("[ImpactSelector] git diff failed: " + e.getMessage());
        }
        return out;
    }

    private static List<String> readStdin() {
        List<String> out = new ArrayList<>();
        try {
            byte[] b = System.in.readAllBytes();
            for (String line : new String(b, StandardCharsets.UTF_8).split("\\R")) {
                if (!line.trim().isEmpty()) { out.add(line.trim()); }
            }
        } catch (Exception ignored) { }
        return out;
    }
}
