package com.example.support;

import org.openqa.selenium.By;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * A lightweight, owned visual-regression check (L3git Advanced). Captures a screenshot of the live
 * page (or one element), compares it pixel-for-pixel against a committed baseline, and fails the
 * step when the difference exceeds a tolerance. No cloud service, no SaaS, no external dependency -
 * pure {@link BufferedImage} diffing, so baselines live in the repo and the check runs fully offline.
 *
 * <p>Layout: baselines are committed at {@code src/test/resources/visual/baselines/<name>.png};
 * each run writes the fresh capture to {@code target/visual/actual/} and, on a mismatch, a
 * highlighted diff to {@code target/visual/diff/}. The FIRST run for a name (or any run with
 * {@code -Dvisual.update=true} / {@code VISUAL_UPDATE=true}) creates/refreshes the baseline and
 * passes - review it, then commit it. A per-channel tolerance absorbs anti-aliasing noise; the
 * overall pass/fail is a mismatched-pixel percentage against the step's threshold. Generated /
 * shipped by L3git.
 */
public final class VisualCheck {

    private VisualCheck() { }

    private static final String BASE_DIR   = "src/test/resources/visual/baselines";
    private static final String ACTUAL_DIR = "target/visual/actual";
    private static final String DIFF_DIR   = "target/visual/diff";

    // Per-channel difference (0-255) tolerated before a pixel counts as "changed". Absorbs the
    // sub-pixel anti-aliasing / font-hinting jitter that makes naive pixel diffs flaky.
    private static final int CHANNEL_TOLERANCE = 32;
    private static final int DIFF_MARK = 0xFF00FF; // magenta

    private static boolean updateMode() {
        String v = System.getenv("VISUAL_UPDATE");
        if (v == null || v.isBlank()) { v = System.getProperty("visual.update", ""); }
        return "true".equalsIgnoreCase(v) || "1".equals(v);
    }

    /**
     * Capture, compare against the baseline, and fail (AssertionError) if the mismatch exceeds
     * {@code maxDiffPercent}. First run (or update mode) creates the baseline and passes.
     *
     * @param driver         the live browser (self-healing tests pass {@code driver.raw()})
     * @param name           baseline id (also the PNG file name)
     * @param scopeCss       CSS selector to screenshot (blank/null = the whole viewport)
     * @param maxDiffPercent max allowed mismatched-pixel percentage (e.g. 0.1)
     */
    public static void assertMatches(WebDriver driver, String name, String scopeCss, double maxDiffPercent) {
        try {
            BufferedImage actual = capture(driver, scopeCss);
            Files.createDirectories(Paths.get(ACTUAL_DIR));
            ImageIO.write(actual, "png", Paths.get(ACTUAL_DIR, name + ".png").toFile());

            Path baseline = Paths.get(BASE_DIR, name + ".png");
            System.out.println("=== Visual check '" + name + "' ===");
            if (updateMode() || !Files.exists(baseline)) {
                Files.createDirectories(baseline.getParent());
                ImageIO.write(actual, "png", baseline.toFile());
                System.out.println(updateMode()
                        ? "Baseline updated (visual.update)."
                        : "Baseline created (first run) - review and commit " + baseline + ".");
                return;
            }

            BufferedImage base = ImageIO.read(baseline.toFile());
            if (base.getWidth() != actual.getWidth() || base.getHeight() != actual.getHeight()) {
                throw new AssertionError(String.format(
                        "Visual '%s': dimensions changed - baseline %dx%d vs actual %dx%d. "
                                + "Re-baseline with -Dvisual.update=true if this is intended.",
                        name, base.getWidth(), base.getHeight(), actual.getWidth(), actual.getHeight()));
            }

            int w = base.getWidth(), h = base.getHeight();
            BufferedImage diff = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
            long diffPixels = 0;
            for (int y = 0; y < h; y++) {
                for (int x = 0; x < w; x++) {
                    int a = base.getRGB(x, y), b = actual.getRGB(x, y);
                    if (pixelDiffers(a, b)) { diffPixels++; diff.setRGB(x, y, DIFF_MARK); }
                    else { diff.setRGB(x, y, washOut(b)); }
                }
            }
            long total = (long) w * h;
            double pct = 100.0 * diffPixels / total;
            Files.createDirectories(Paths.get(DIFF_DIR));
            File diffFile = Paths.get(DIFF_DIR, name + ".png").toFile();
            ImageIO.write(diff, "png", diffFile);
            System.out.printf("Difference: %.4f%% (%d / %d px), threshold %.4f%%.%n",
                    pct, diffPixels, total, maxDiffPercent);

            if (pct > maxDiffPercent) {
                throw new AssertionError(String.format(
                        "Visual '%s' changed by %.4f%% (> %.4f%% allowed). See the highlighted diff: %s",
                        name, pct, maxDiffPercent, diffFile.getPath()));
            }
        } catch (IOException e) {
            throw new RuntimeException("Visual check '" + name + "' failed on image I/O", e);
        }
    }

    private static boolean pixelDiffers(int a, int b) {
        int dr = Math.abs(((a >> 16) & 0xFF) - ((b >> 16) & 0xFF));
        int dg = Math.abs(((a >> 8) & 0xFF) - ((b >> 8) & 0xFF));
        int db = Math.abs((a & 0xFF) - (b & 0xFF));
        return dr > CHANNEL_TOLERANCE || dg > CHANNEL_TOLERANCE || db > CHANNEL_TOLERANCE;
    }

    // Unchanged pixels are washed toward light gray so the magenta changes stand out in the diff PNG.
    private static int washOut(int rgb) {
        int r = (rgb >> 16) & 0xFF, g = (rgb >> 8) & 0xFF, b = rgb & 0xFF;
        int lum = (int) (0.3 * r + 0.59 * g + 0.11 * b);
        lum = 160 + lum / 4;
        if (lum > 255) { lum = 255; }
        return (lum << 16) | (lum << 8) | lum;
    }

    private static BufferedImage capture(WebDriver driver, String scopeCss) throws IOException {
        byte[] png;
        if (scopeCss != null && !scopeCss.trim().isEmpty()) {
            WebElement el = driver.findElement(By.cssSelector(scopeCss.trim()));
            png = el.getScreenshotAs(OutputType.BYTES);
        } else {
            png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
        }
        BufferedImage img = ImageIO.read(new ByteArrayInputStream(png));
        if (img == null) { throw new IOException("could not decode the screenshot PNG"); }
        return img;
    }
}
