/**
 * Unit-Test für scripts/4_yAxis_rotation_reconstruction.py
 *
 * Das Python-Skript wird via child_process aufgerufen.
 * stdout enthält das JSON-Ergebnis (letzte Zeile – davor ggf. Leerzeilen).
 * stderr enthält menschenlesbare Zusammenfassung (wird ignoriert).
 *
 * Test-Daten: test/y-axis-finder-examples/ (drei Timestamps → Arm1-Bogen)
 *
 * Erwartete Werte stammen aus verifiziertem Lauf (2026-06-12):
 *   Marker 197, 218, 219 → rotieren  (12 Punkte, 3 × 4 Ecken)
 *   Marker 201, 204      → gefiltert (Bewegung < 10 mm)
 *   axisDir ≈ [0.9995, 0.029, -0.015]  (fast reine X-Achse des Roboters)
 */

const { execFileSync } = require('child_process');
const path = require('path');

const SCRIPT   = path.join(__dirname, '..', 'scripts', '4_yAxis_rotation_reconstruction.py');
const EXAMPLES = path.join(__dirname, 'y-axis-finder-examples');
const POS_A    = path.join(EXAMPLES, '20260612_190019');
const POS_B    = path.join(EXAMPLES, '20260612_190104');
const POS_C    = path.join(EXAMPLES, '20260612_190241');

// ── Hilfsfunktion ──────────────────────────────────────────────────────────────
const { spawnSync } = require('child_process');

/**
 * Ruft das Python-Skript auf und gibt das geparste JSON zurück.
 * Verwendet spawnSync (statt execFileSync) damit ein Fehler-Exit-Code (ok:false)
 * nicht sofort eine Exception auslöst – wir parsen das stdout trotzdem.
 * stdout kann mehrere Zeilen haben – JSON ist die letzte nicht-leere Zeile.
 */
function runScript(posA, posB, posC, extraArgs = []) {
    const proc = spawnSync('python3', [SCRIPT, posA, posB, posC, ...extraArgs], {
        encoding: 'utf-8',
        // stderr läuft zum Terminal durch → menschenlesbarer Summary sichtbar
    });
    if (proc.error) throw proc.error;  // z. B. python3 nicht gefunden

    const stdout = proc.stdout || '';
    const lastJsonLine = stdout
        .trim()
        .split('\n')
        .filter(l => l.trim().startsWith('{'))
        .at(-1);

    if (!lastJsonLine) {
        throw new Error(
            `Kein JSON in stdout.\nstdout: ${stdout}\nstderr: ${proc.stderr}`
        );
    }
    return JSON.parse(lastJsonLine);
}

// ── Haupt-Test-Suite ──────────────────────────────────────────────────────────
describe('4_yAxis_rotation_reconstruction.py – Arm1-Testdaten (190019 / 190104 / 190241)', () => {

    /** Einmalig ausführen – dauert ca. 1 s */
    let r;
    beforeAll(() => {
        r = runScript(POS_A, POS_B, POS_C);
    });

    // ── Grundstatus ───────────────────────────────────────────────────────────
    test('Berechnung erfolgreich (ok: true)', () => {
        expect(r.ok).toBe(true);
    });

    // ── Marker-Anzahlen ───────────────────────────────────────────────────────
    test('5 gemeinsame fremd-Marker erkannt', () => {
        expect(r.numMarkersCommon).toBe(5);
        expect(r.commonMarkerIds).toEqual([197, 201, 204, 218, 219]);
    });

    test('3 Marker tatsächlich genutzt (197, 218, 219)', () => {
        expect(r.numMarkersUsed).toBe(3);
        expect(r.usedMarkerIds).toEqual([197, 218, 219]);
    });

    test('12 Punkte (3 Marker × 4 Ecken)', () => {
        expect(r.numPoints).toBe(12);
    });

    // ── Filterung ─────────────────────────────────────────────────────────────
    test('2 Marker wegen zu geringer Bewegung gefiltert (201, 204)', () => {
        expect(r.skipped).toHaveLength(2);

        const skippedIds = r.skipped.map(s => s.marker_id).sort((a, b) => a - b);
        expect(skippedIds).toEqual([201, 204]);

        // Beide mit korrektem Grund
        r.skipped.forEach(s => {
            expect(s.reason).toMatch(/Bewegung zu gering/);
            expect(s.max_movement_mm).toBeLessThan(10.0);   // unter dem Threshold
        });
    });

    // ── Residuen ──────────────────────────────────────────────────────────────
    test('Abstandsresiduen: Mittelwert < 10 mm', () => {
        expect(r.residual_dist_mean_mm).toBeLessThan(10);
    });

    test('Abstandsresiduen: Maximum < 15 mm', () => {
        expect(r.residual_dist_max_mm).toBeLessThan(15);
    });

    test('Winkelresiduen: Mittelwert < 5°', () => {
        expect(r.residual_angle_mean_deg).toBeLessThan(5);
    });

    test('Winkelresiduen: Maximum < 7°', () => {
        expect(r.residual_angle_max_deg).toBeLessThan(7);
    });

    // ── Achsenrichtung ────────────────────────────────────────────────────────
    test('axisDir ist ein Einheitsvektor (|axisDir| ≈ 1)', () => {
        const [ax, ay, az] = r.axisDir;
        const len = Math.sqrt(ax * ax + ay * ay + az * az);
        expect(len).toBeCloseTo(1.0, 4);   // ≤ 0.00005 Abweichung
    });

    test('Achse liegt fast auf X-Achse des Roboters (axisDir[0] > 0.99)', () => {
        // Physikalisch: Arm1 schwingt auf/ab → Drehachse = X-Richtung
        expect(r.axisDir[0]).toBeGreaterThan(0.99);
    });

    test('axisDir hat 3 Komponenten', () => {
        expect(r.axisDir).toHaveLength(3);
    });

    test('axisPoint_mm hat 3 Komponenten', () => {
        expect(r.axisPoint_mm).toHaveLength(3);
    });

    // ── Marker-Ergebnis-Struktur ──────────────────────────────────────────────
    test('markerResults enthält genau 3 Einträge (verwendete Marker)', () => {
        expect(r.markerResults).toHaveLength(3);
    });

    test('jeder Marker-Eintrag hat n_points_used === 4', () => {
        r.markerResults.forEach(mr => {
            expect(mr.n_points_used).toBe(4);
            expect(mr.circumcenter_mean_mm).toHaveLength(3);
        });
    });
});

// ── Parametertest: benutzerdefinierter min-movement-Threshold ─────────────────
describe('4_yAxis_rotation_reconstruction.py – min-movement-Parameter', () => {

    test('mit --min-movement 0 werden alle 5 Marker genutzt (kein Filter)', () => {
        const r = runScript(POS_A, POS_B, POS_C, ['--min-movement', '0']);
        // Alle 5 gemeinsamen Marker werden versucht; Ergebnis ist schlechter
        expect(r.ok).toBe(true);
        expect(r.numMarkersUsed).toBe(5);
        expect(r.numMarkersCommon).toBe(5);
        // skipped enthält nur noch ggf. degenerierte Punkte (keine Bewegungs-Skips)
        const movementSkips = r.skipped.filter(s => s.reason && s.reason.includes('Bewegung zu gering'));
        expect(movementSkips).toHaveLength(0);
    });

    test('mit --min-movement 1000 werden alle Marker gefiltert → ok: false', () => {
        // Die rotierenden Marker (197, 218, 219) bewegen sich >100 mm,
        // erst ab 1000 mm werden auch sie herausgefiltert.
        const r = runScript(POS_A, POS_B, POS_C, ['--min-movement', '1000']);
        expect(r.ok).toBe(false);
        expect(r.error).toBeDefined();
    });
});
