"""Tests for touchstone module: format conversion, parsing, S-parameter extraction."""

from pathlib import Path

import numpy as np
import pytest

from mcltspice.touchstone import (
    _detect_ports,
    _to_complex,
    get_s_parameter,
    parse_touchstone,
    s_param_to_db,
)


class TestToComplex:
    def test_ri_format(self):
        """RI: (real, imag) -> complex."""
        c = _to_complex(3.0, 4.0, "RI")
        assert c == complex(3.0, 4.0)

    def test_ma_format(self):
        """MA: (magnitude, angle_deg) -> complex."""
        c = _to_complex(1.0, 0.0, "MA")
        assert c == pytest.approx(complex(1.0, 0.0), abs=1e-10)

        c90 = _to_complex(1.0, 90.0, "MA")
        assert c90.real == pytest.approx(0.0, abs=1e-10)
        assert c90.imag == pytest.approx(1.0, abs=1e-10)

    def test_db_format(self):
        """DB: (mag_db, angle_deg) -> complex."""
        # 0 dB = magnitude 1.0
        c = _to_complex(0.0, 0.0, "DB")
        assert abs(c) == pytest.approx(1.0)

        # 20 dB = magnitude 10.0
        c20 = _to_complex(20.0, 0.0, "DB")
        assert abs(c20) == pytest.approx(10.0, rel=0.01)

    def test_unknown_format_raises(self):
        with pytest.raises(ValueError, match="Unknown format"):
            _to_complex(1.0, 0.0, "XY")


class TestDetectPorts:
    @pytest.mark.parametrize(
        "suffix, expected",
        [
            (".s1p", 1),
            (".s2p", 2),
            (".s3p", 3),
            (".s4p", 4),
            (".S2P", 2),  # case insensitive
        ],
    )
    def test_valid_extensions(self, suffix, expected):
        p = Path(f"test{suffix}")
        assert _detect_ports(p) == expected

    def test_invalid_extension(self):
        with pytest.raises(ValueError, match="Cannot determine port count"):
            _detect_ports(Path("test.txt"))


class TestParseTouchstone:
    def test_parse_s2p(self, tmp_s2p_file):
        """Parse a synthetic .s2p file and verify structure."""
        data = parse_touchstone(tmp_s2p_file)
        assert data.n_ports == 2
        assert data.parameter_type == "S"
        assert data.format_type == "MA"
        assert data.reference_impedance == 50.0
        assert len(data.frequencies) == 3
        assert data.data.shape == (3, 2, 2)

    def test_frequencies_in_hz(self, tmp_s2p_file):
        """Frequencies should be converted to Hz (from GHz)."""
        data = parse_touchstone(tmp_s2p_file)
        # First freq is 1.0 GHz = 1e9 Hz
        assert data.frequencies[0] == pytest.approx(1e9)
        assert data.frequencies[1] == pytest.approx(2e9)
        assert data.frequencies[2] == pytest.approx(3e9)

    def test_s11_values(self, tmp_s2p_file):
        """S11 at first frequency should match input: mag=0.5, angle=-30."""
        data = parse_touchstone(tmp_s2p_file)
        s11 = data.data[0, 0, 0]
        assert abs(s11) == pytest.approx(0.5, rel=0.01)
        assert np.degrees(np.angle(s11)) == pytest.approx(-30.0, abs=1.0)

    def test_comments_parsed(self, tmp_s2p_file):
        data = parse_touchstone(tmp_s2p_file)
        assert len(data.comments) > 0

    def test_s1p_file(self, tmp_path):
        """Parse a minimal .s1p file."""
        content = (
            "# MHZ S RI R 50\n"
            "100  0.5 0.3\n"
            "200  0.4 0.2\n"
        )
        p = tmp_path / "test.s1p"
        p.write_text(content)
        data = parse_touchstone(p)
        assert data.n_ports == 1
        assert data.data.shape == (2, 1, 1)
        # 100 MHz = 100e6 Hz
        assert data.frequencies[0] == pytest.approx(100e6)

    def test_db_format_file(self, tmp_path):
        """Parse a .s1p file in DB format."""
        content = (
            "# GHZ S DB R 50\n"
            "1.0  -3.0 -45\n"
            "2.0  -6.0 -90\n"
        )
        p = tmp_path / "dbtest.s1p"
        p.write_text(content)
        data = parse_touchstone(p)
        assert data.format_type == "DB"
        # -3 dB -> magnitude ~ 0.707
        assert abs(data.data[0, 0, 0]) == pytest.approx(10 ** (-3.0 / 20.0), rel=0.01)


class TestSParamToDb:
    def test_unity_magnitude(self):
        """Magnitude 1.0 -> 0 dB."""
        vals = np.array([1.0 + 0j])
        db = s_param_to_db(vals)
        assert db[0] == pytest.approx(0.0, abs=0.01)

    def test_known_magnitude(self):
        """Magnitude 0.1 -> -20 dB."""
        vals = np.array([0.1 + 0j])
        db = s_param_to_db(vals)
        assert db[0] == pytest.approx(-20.0, abs=0.1)

    def test_zero_magnitude(self):
        """Zero magnitude should not produce -inf (floored)."""
        vals = np.array([0.0 + 0j])
        db = s_param_to_db(vals)
        assert np.isfinite(db[0])
        assert db[0] < -200


class TestGetSParameter:
    def test_1_based_indexing(self, tmp_s2p_file):
        data = parse_touchstone(tmp_s2p_file)
        freqs, vals = get_s_parameter(data, 1, 1)
        assert len(freqs) == 3
        assert len(vals) == 3

    def test_s21(self, tmp_s2p_file):
        data = parse_touchstone(tmp_s2p_file)
        # S21 is stored at (row=0, col=1) -> get_s_parameter(data, 1, 2)
        # because the parser iterates row then col, and Touchstone 2-port
        # order is S11, S21, S12, S22 -> (0,0), (0,1), (1,0), (1,1)
        freqs, vals = get_s_parameter(data, 1, 2)
        # S21 at first freq: mag=0.9, angle=-10
        assert abs(vals[0]) == pytest.approx(0.9, rel=0.01)

    def test_out_of_range_row(self, tmp_s2p_file):
        data = parse_touchstone(tmp_s2p_file)
        with pytest.raises(IndexError, match="Row index"):
            get_s_parameter(data, 3, 1)

    def test_out_of_range_col(self, tmp_s2p_file):
        data = parse_touchstone(tmp_s2p_file)
        with pytest.raises(IndexError, match="Column index"):
            get_s_parameter(data, 1, 3)

    def test_zero_index_raises(self, tmp_s2p_file):
        data = parse_touchstone(tmp_s2p_file)
        with pytest.raises(IndexError):
            get_s_parameter(data, 0, 1)
