"""Tests for waveform_math module: RMS, peak-to-peak, FFT, THD, bandwidth, settling, rise time."""

import numpy as np
import pytest

from mcltspice.waveform_math import (
    compute_bandwidth,
    compute_fft,
    compute_peak_to_peak,
    compute_rise_time,
    compute_rms,
    compute_settling_time,
    compute_thd,
)


class TestComputeRms:
    def test_dc_signal_exact(self, dc_signal):
        """RMS of a DC signal equals its DC value."""
        rms = compute_rms(dc_signal)
        assert rms == pytest.approx(3.3, abs=1e-10)

    def test_sine_vpk_over_sqrt2(self, sine_1khz):
        """RMS of a pure sine (1 V peak) should be 1/sqrt(2)."""
        rms = compute_rms(sine_1khz)
        assert rms == pytest.approx(1.0 / np.sqrt(2), rel=0.01)

    def test_empty_signal(self):
        """RMS of an empty array is 0."""
        assert compute_rms(np.array([])) == 0.0

    def test_single_sample(self):
        """RMS of a single sample equals abs(sample)."""
        assert compute_rms(np.array([5.0])) == pytest.approx(5.0)

    def test_complex_signal(self):
        """RMS uses only the real part of complex data."""
        sig = np.array([3.0 + 4j, 3.0 + 4j])
        rms = compute_rms(sig)
        assert rms == pytest.approx(3.0)


class TestComputePeakToPeak:
    def test_sine_wave(self, sine_1khz):
        """Peak-to-peak of a 1 V peak sine should be ~2 V."""
        result = compute_peak_to_peak(sine_1khz)
        assert result["peak_to_peak"] == pytest.approx(2.0, rel=0.01)
        assert result["max"] == pytest.approx(1.0, rel=0.01)
        assert result["min"] == pytest.approx(-1.0, rel=0.01)
        assert result["mean"] == pytest.approx(0.0, abs=0.01)

    def test_dc_signal(self, dc_signal):
        """Peak-to-peak of a DC signal is 0."""
        result = compute_peak_to_peak(dc_signal)
        assert result["peak_to_peak"] == pytest.approx(0.0)

    def test_empty_signal(self):
        result = compute_peak_to_peak(np.array([]))
        assert result["peak_to_peak"] == 0.0


class TestComputeFft:
    def test_known_sine_peak_at_correct_freq(self, time_array, sine_1khz):
        """A 1 kHz sine should produce a dominant peak at 1 kHz."""
        result = compute_fft(time_array, sine_1khz)
        assert result["fundamental_freq"] == pytest.approx(1000, rel=0.05)
        assert result["dc_offset"] == pytest.approx(0.0, abs=0.01)

    def test_dc_offset_detection(self, time_array):
        """A signal with DC offset should report correct dc_offset."""
        offset = 2.5
        sig = offset + np.sin(2 * np.pi * 1000 * time_array)
        result = compute_fft(time_array, sig)
        assert result["dc_offset"] == pytest.approx(offset, rel=0.05)

    def test_short_signal(self):
        """Very short signals return empty results."""
        result = compute_fft(np.array([0.0]), np.array([1.0]))
        assert result["frequencies"] == []
        assert result["fundamental_freq"] == 0.0

    def test_zero_dt(self):
        """Time array with zero duration returns gracefully."""
        result = compute_fft(np.array([1.0, 1.0]), np.array([1.0, 2.0]))
        assert result["frequencies"] == []


class TestComputeThd:
    def test_pure_sine_low_thd(self, time_array, sine_1khz):
        """A pure sine wave should have very low THD."""
        result = compute_thd(time_array, sine_1khz)
        assert result["thd_percent"] < 1.0
        assert result["fundamental_freq"] == pytest.approx(1000, rel=0.05)

    def test_clipped_sine_high_thd(self, time_array, sine_1khz):
        """A hard-clipped sine should have significantly higher THD."""
        clipped = np.clip(sine_1khz, -0.5, 0.5)
        result = compute_thd(time_array, clipped)
        # Clipping at 50% introduces substantial harmonics
        assert result["thd_percent"] > 10.0

    def test_short_signal(self):
        result = compute_thd(np.array([0.0]), np.array([1.0]))
        assert result["thd_percent"] == 0.0


class TestComputeBandwidth:
    def test_lowpass_cutoff(self, ac_frequency, lowpass_response):
        """Lowpass with fc=1kHz should report bandwidth near 1 kHz."""
        result = compute_bandwidth(ac_frequency, lowpass_response)
        assert result["bandwidth_hz"] == pytest.approx(1000, rel=0.1)
        assert result["type"] == "lowpass"

    def test_all_above_cutoff(self, ac_frequency):
        """If all magnitudes are above -3dB level, bandwidth spans entire range."""
        flat = np.zeros_like(ac_frequency)
        result = compute_bandwidth(ac_frequency, flat)
        assert result["bandwidth_hz"] > 0

    def test_short_input(self):
        result = compute_bandwidth(np.array([1.0]), np.array([0.0]))
        assert result["bandwidth_hz"] == 0.0

    def test_bandpass_shape(self):
        """A peaked response should be detected as bandpass."""
        fc = 10_000.0
        Q = 5.0  # Q factor => BW = fc/Q = 2000 Hz
        bw_expected = fc / Q
        freq = np.logspace(2, 6, 2000)
        # Second-order bandpass: H(s) = (s/wn/Q) / (s^2/wn^2 + s/wn/Q + 1)
        wn = 2 * np.pi * fc
        s = 1j * 2 * np.pi * freq
        H = (s / wn / Q) / (s**2 / wn**2 + s / wn / Q + 1)
        mag_db = 20.0 * np.log10(np.abs(H))
        result = compute_bandwidth(freq, mag_db)
        assert result["type"] == "bandpass"
        assert result["bandwidth_hz"] == pytest.approx(bw_expected, rel=0.15)


class TestComputeSettlingTime:
    def test_already_settled(self, time_array, dc_signal):
        """A constant signal is already settled at t=0."""
        t = np.linspace(0, 0.01, len(dc_signal))
        result = compute_settling_time(t, dc_signal, final_value=3.3)
        assert result["settled"] is True
        assert result["settling_time"] == 0.0

    def test_step_response(self, time_array, step_signal):
        """Step response should settle after the transient."""
        result = compute_settling_time(time_array, step_signal, final_value=1.0)
        assert result["settled"] is True
        assert result["settling_time"] > 0

    def test_never_settles(self, time_array, sine_1khz):
        """An oscillating signal never settles to a DC value."""
        result = compute_settling_time(time_array, sine_1khz, final_value=0.5)
        assert result["settled"] is False

    def test_short_signal(self):
        result = compute_settling_time(np.array([0.0]), np.array([1.0]))
        assert result["settled"] is False


class TestComputeRiseTime:
    def test_fast_step(self):
        """A fast rising step should have a short rise time."""
        t = np.linspace(0, 1e-3, 10000)
        # Step with very fast exponential rise
        sig = np.where(t > 0.1e-3, 1.0 - np.exp(-(t - 0.1e-3) / 20e-6), 0.0)
        result = compute_rise_time(t, sig)
        assert result["rise_time"] > 0
        # 10-90% rise time of RC = ~2.2 * tau
        assert result["rise_time"] == pytest.approx(2.2 * 20e-6, rel=0.2)

    def test_no_swing(self):
        """Flat signal has zero rise time."""
        t = np.linspace(0, 1, 100)
        sig = np.ones(100) * 5.0
        result = compute_rise_time(t, sig)
        assert result["rise_time"] == 0.0

    def test_short_signal(self):
        result = compute_rise_time(np.array([0.0]), np.array([0.0]))
        assert result["rise_time"] == 0.0
