"""Integration tests that run actual LTspice simulations.

These tests require LTspice and Wine to be installed. Skip with:
    pytest -m 'not integration'
"""

import tempfile
from pathlib import Path

import numpy as np
import pytest

from mcltspice.asc_generator import (
    generate_colpitts_oscillator as generate_colpitts_asc,
)
from mcltspice.asc_generator import (
    generate_common_emitter_amp as generate_ce_amp_asc,
)
from mcltspice.asc_generator import (
    generate_non_inverting_amp as generate_noninv_amp_asc,
)
from mcltspice.asc_generator import (
    generate_rc_lowpass as generate_rc_lowpass_asc,
)
from mcltspice.runner import run_simulation
from mcltspice.waveform_math import compute_bandwidth, compute_rms


@pytest.mark.integration
class TestRCLowpass:
    """End-to-end test: RC lowpass filter -> simulate -> verify -3dB point."""

    async def test_rc_lowpass_bandwidth(self, ltspice_available):
        """Generate RC lowpass (R=1k, C=100n), simulate, verify fc ~ 1.6 kHz."""
        # Generate schematic
        sch = generate_rc_lowpass_asc(r="1k", c="100n")

        with tempfile.TemporaryDirectory() as tmpdir:
            asc_path = Path(tmpdir) / "rc_lowpass.asc"
            sch.save(asc_path)

            # Simulate
            result = await run_simulation(asc_path)
            assert result.success, f"Simulation failed: {result.error or result.stderr}"
            assert result.raw_data is not None, "No raw data produced"

            # Find frequency and V(out) variables
            raw = result.raw_data
            freq_idx = None
            vout_idx = None
            for var in raw.variables:
                if var.name.lower() == "frequency":
                    freq_idx = var.index
                elif var.name.lower() == "v(out)":
                    vout_idx = var.index

            assert freq_idx is not None, (
                f"No frequency variable. Variables: {[v.name for v in raw.variables]}"
            )
            assert vout_idx is not None, (
                f"No V(out) variable. Variables: {[v.name for v in raw.variables]}"
            )

            # Extract data
            freq = np.abs(raw.data[freq_idx])
            mag_complex = raw.data[vout_idx]
            mag_db = 20.0 * np.log10(np.maximum(np.abs(mag_complex), 1e-30))

            # Compute bandwidth
            bw = compute_bandwidth(freq, mag_db)

            # Expected: fc = 1/(2*pi*1000*100e-9) ~ 1591 Hz
            # Allow 20% tolerance for simulation differences
            expected_fc = 1.0 / (2 * np.pi * 1000 * 100e-9)
            assert bw["bandwidth_hz"] is not None, "Could not compute bandwidth"
            assert abs(bw["bandwidth_hz"] - expected_fc) / expected_fc < 0.2, (
                f"Bandwidth {bw['bandwidth_hz']:.0f} Hz too far from expected {expected_fc:.0f} Hz"
            )


@pytest.mark.integration
class TestNonInvertingAmp:
    """End-to-end test: non-inverting amp -> simulate -> verify gain."""

    async def test_noninv_amp_gain(self, ltspice_available):
        """Generate non-inverting amp (Rf=100k, Rin=10k), verify gain ~ 11 (20.8 dB)."""
        sch = generate_noninv_amp_asc(rin="10k", rf="100k")

        with tempfile.TemporaryDirectory() as tmpdir:
            asc_path = Path(tmpdir) / "noninv_amp.asc"
            sch.save(asc_path)

            result = await run_simulation(asc_path)
            assert result.success, f"Simulation failed: {result.error or result.stderr}"
            assert result.raw_data is not None

            raw = result.raw_data
            freq_idx = None
            vout_idx = None
            for var in raw.variables:
                if var.name.lower() == "frequency":
                    freq_idx = var.index
                elif var.name.lower() == "v(out)":
                    vout_idx = var.index

            assert freq_idx is not None
            assert vout_idx is not None

            _freq = np.abs(raw.data[freq_idx])  # noqa: F841 — kept for debug
            mag = np.abs(raw.data[vout_idx])
            mag_db = 20.0 * np.log10(np.maximum(mag, 1e-30))

            # At low frequency, gain should be 1 + 100k/10k = 11 = 20.83 dB
            # Use first few points (low frequency)
            low_freq_gain_db = float(np.mean(mag_db[:5]))
            expected_gain_db = 20 * np.log10(11)  # 20.83 dB

            assert abs(low_freq_gain_db - expected_gain_db) < 2.0, (
                f"Low-freq gain {low_freq_gain_db:.1f} dB, expected ~{expected_gain_db:.1f} dB"
            )


@pytest.mark.integration
class TestCommonEmitterAmp:
    """End-to-end test: CE amplifier -> simulate transient -> verify output exists."""

    async def test_ce_amp_output(self, ltspice_available):
        """Generate CE amp, simulate transient, verify output has AC content."""
        sch = generate_ce_amp_asc()

        with tempfile.TemporaryDirectory() as tmpdir:
            asc_path = Path(tmpdir) / "ce_amp.asc"
            sch.save(asc_path)

            result = await run_simulation(asc_path)
            assert result.success, f"Simulation failed: {result.error or result.stderr}"
            assert result.raw_data is not None

            raw = result.raw_data
            time_idx = None
            vout_idx = None
            for var in raw.variables:
                if var.name.lower() == "time":
                    time_idx = var.index
                elif var.name.lower() == "v(out)":
                    vout_idx = var.index

            assert time_idx is not None
            assert vout_idx is not None

            sig = np.real(raw.data[vout_idx])

            # Output should not be DC-only -- check peak-to-peak > threshold
            pp = float(np.max(sig) - np.min(sig))
            assert pp > 0.01, (
                f"Output appears DC-only (peak-to-peak={pp:.4f}V). "
                "Expected amplified AC signal."
            )

            # RMS should be non-trivial
            rms = float(compute_rms(sig))
            assert rms > 0.01, f"Output RMS too low: {rms:.4f}V"


@pytest.mark.integration
class TestColpittsOscillator:
    """End-to-end test: Colpitts oscillator -> simulate -> verify oscillation."""

    async def test_colpitts_oscillation(self, ltspice_available):
        """Generate Colpitts oscillator, verify oscillation near expected frequency."""
        sch = generate_colpitts_asc()

        with tempfile.TemporaryDirectory() as tmpdir:
            asc_path = Path(tmpdir) / "colpitts.asc"
            sch.save(asc_path)

            result = await run_simulation(asc_path)
            assert result.success, f"Simulation failed: {result.error or result.stderr}"
            assert result.raw_data is not None

            raw = result.raw_data
            time_idx = None
            vcol_idx = None
            for var in raw.variables:
                if var.name.lower() == "time":
                    time_idx = var.index
                elif var.name.lower() == "v(out)":
                    vcol_idx = var.index
                elif "collector" in var.name.lower() and vcol_idx is None:
                    vcol_idx = var.index

            assert time_idx is not None

            assert vcol_idx is not None, (
                f"No V(out) or collector signal. Variables: {[v.name for v in raw.variables]}"
            )

            sig = np.real(raw.data[vcol_idx])

            # Oscillator output should have significant AC content
            pp = float(np.max(sig) - np.min(sig))
            assert pp > 0.1, (
                f"Output peak-to-peak {pp:.3f}V too small -- oscillator may not have started"
            )

            # Expected frequency: f = 1/(2*pi*sqrt(L*C1*C2/(C1+C2)))
            # With L=1u, C1=C2=100p: Cseries = 50p
            # f = 1/(2*pi*sqrt(1e-6 * 50e-12)) ~ 22.5 MHz
            # This is quite high, but we just verify oscillation exists
