package org.wickra;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

/**
 * Golden-fixture parity: replay the shared {@code testdata/golden} input series
 * through the Java FFI and assert every value matches the Rust reference output.
 * Where the archetype tests only check finiteness, this pins exact values, so a
 * wiring bug (swapped parameter, wrong multi-output field) is caught. Fixtures
 * are generated by {@code cargo run -p wickra-examples --bin gen_golden}.
 */
class GoldenTests {
    private static final double TOL = 1e-6;

    private static Path goldenDir() {
        File d = new File("").getAbsoluteFile();
        while (d != null) {
            File g = new File(d, "testdata/golden");
            if (g.isDirectory()) {
                return g.toPath();
            }
            d = d.getParentFile();
        }
        throw new IllegalStateException("testdata/golden not found from " + new File("").getAbsolutePath());
    }

    private static List<String[]> readCsv(String name) throws Exception {
        List<String> lines = Files.readAllLines(goldenDir().resolve(name + ".csv"));
        List<String[]> rows = new ArrayList<>();
        for (int i = 1; i < lines.size(); i++) { // skip header
            if (!lines.get(i).isEmpty()) {
                rows.add(lines.get(i).split(","));
            }
        }
        return rows;
    }

    private static double cell(String s) {
        return s.equals("nan") ? Double.NaN : Double.parseDouble(s);
    }

    private static double[][] input() throws Exception {
        List<String[]> rows = readCsv("input");
        double[][] out = new double[rows.size()][];
        for (int i = 0; i < rows.size(); i++) {
            String[] r = rows.get(i);
            double[] v = new double[r.length];
            for (int j = 0; j < r.length; j++) {
                v[j] = Double.parseDouble(r[j]);
            }
            out[i] = v;
        }
        return out;
    }

    private static void close(double got, double want, int row, String field) {
        if (Double.isNaN(want)) {
            assertTrue(Double.isNaN(got), "row " + row + " " + field + ": expected warmup/NaN, got " + got);
            return;
        }
        double tol = TOL * Math.max(1.0, Math.abs(want));
        assertTrue(Math.abs(got - want) <= tol, "row " + row + " " + field + ": got " + got + " want " + want);
    }

    @Test
    void scalarMatchesGolden() throws Exception {
        double[][] in = input();
        try (Sma sma = new Sma(14)) {
            List<String[]> e = readCsv("sma");
            for (int i = 0; i < in.length; i++) {
                close(sma.update(in[i][3]), cell(e.get(i)[0]), i, "sma");
            }
        }
        try (Ema ema = new Ema(14)) {
            List<String[]> e = readCsv("ema");
            for (int i = 0; i < in.length; i++) {
                close(ema.update(in[i][3]), cell(e.get(i)[0]), i, "ema");
            }
        }
        try (Rsi rsi = new Rsi(14)) {
            List<String[]> e = readCsv("rsi");
            for (int i = 0; i < in.length; i++) {
                close(rsi.update(in[i][3]), cell(e.get(i)[0]), i, "rsi");
            }
        }
    }

    @Test
    void candleAtrMatchesGolden() throws Exception {
        double[][] in = input();
        List<String[]> e = readCsv("atr");
        try (Atr atr = new Atr(14)) {
            for (int i = 0; i < in.length; i++) {
                close(atr.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "atr");
            }
        }
    }

    @Test
    void pairwiseBetaMatchesGolden() throws Exception {
        double[][] in = input();
        List<String[]> e = readCsv("beta");
        try (Beta beta = new Beta(20)) {
            for (int i = 0; i < in.length; i++) {
                // generator fed (close, open)
                close(beta.update(in[i][3], in[i][0]), cell(e.get(i)[0]), i, "beta");
            }
        }
    }

    @Test
    void macdMatchesGolden() throws Exception {
        double[][] in = input();
        List<String[]> e = readCsv("macd");
        try (MacdIndicator macd = new MacdIndicator(12, 26, 9)) {
            for (int i = 0; i < in.length; i++) {
                MacdOutput o = macd.update(in[i][3]);
                String[] row = e.get(i);
                if (row[0].equals("nan")) {
                    assertNull(o, "row " + i + " macd: expected warmup");
                    continue;
                }
                assertNotNull(o, "row " + i + " macd: expected value");
                close(o.macd(), cell(row[0]), i, "macd.macd");
                close(o.signal(), cell(row[1]), i, "macd.signal");
                close(o.histogram(), cell(row[2]), i, "macd.histogram");
            }
        }
    }

    @Test
    void adxMatchesGolden() throws Exception {
        double[][] in = input();
        List<String[]> e = readCsv("adx");
        try (Adx adx = new Adx(14)) {
            for (int i = 0; i < in.length; i++) {
                AdxOutput o = adx.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i);
                String[] row = e.get(i);
                if (row[0].equals("nan")) {
                    assertNull(o, "row " + i + " adx: expected warmup");
                    continue;
                }
                assertNotNull(o, "row " + i + " adx: expected value");
                close(o.plusDi(), cell(row[0]), i, "adx.plus_di");
                close(o.minusDi(), cell(row[1]), i, "adx.minus_di");
                close(o.adx(), cell(row[2]), i, "adx.adx");
            }
        }
    }

    // The four de-duplicated indicators, pinned against the Rust reference.

    @Test
    void adOscillatorMatchesGolden() throws Exception {
        double[][] in = input();
        List<String[]> e = readCsv("ad_oscillator");
        try (AdOscillator ad = new AdOscillator()) {
            for (int i = 0; i < in.length; i++) {
                close(ad.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "ad_oscillator");
            }
        }
    }

    @Test
    void intradayIntensityMatchesGolden() throws Exception {
        double[][] in = input();
        List<String[]> e = readCsv("intraday_intensity");
        try (IntradayIntensity ii = new IntradayIntensity()) {
            for (int i = 0; i < in.length; i++) {
                close(ii.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "intraday_intensity");
            }
        }
    }

    @Test
    void awesomeOscillatorHistogramMatchesGolden() throws Exception {
        double[][] in = input();
        List<String[]> e = readCsv("awesome_oscillator_histogram");
        try (AwesomeOscillatorHistogram aoh = new AwesomeOscillatorHistogram(5, 34, 1)) {
            for (int i = 0; i < in.length; i++) {
                close(aoh.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "awesome_oscillator_histogram");
            }
        }
    }

    @Test
    void averageDrawdownMatchesGolden() throws Exception {
        double[][] in = input();
        List<String[]> e = readCsv("average_drawdown");
        try (AverageDrawdown avg = new AverageDrawdown(20)) {
            for (int i = 0; i < in.length; i++) {
                // generator fed the close column as the equity-curve sample.
                close(avg.update(in[i][3]), cell(e.get(i)[0]), i, "average_drawdown");
            }
        }
    }
}
