# Copyright 2026 Gaofeng Fan
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for Testbench and Analysis classes."""

import re
from unittest.mock import patch
from analogpy import (
    Testbench, Circuit, nmos, pmos, generate_spectre,
    DC, AC, Transient, Noise,
    PSS, PAC, PNoise, PXF, SP, XF, PZ, Sweep, HB, DCMatch, ACMatch
)
from analogpy.devices import vsource


def make_inverter():
    """Helper to create an inverter circuit."""
    inv = Circuit(name="inverter", ports=["inp", "out", "vdd", "vss"])
    inv.add_instance(nmos, "MN", d=inv.net("out"), g=inv.net("inp"),
                    s=inv.net("vss"), b=inv.net("vss"), w=1e-6, l=180e-9)
    inv.add_instance(pmos, "MP", d=inv.net("out"), g=inv.net("inp"),
                    s=inv.net("vdd"), b=inv.net("vdd"), w=2e-6, l=180e-9)
    return inv


def test_testbench_basic():
    """Test basic Testbench creation with explicit name."""
    tb = Testbench("tb_test")
    assert tb.name == "tb_test"
    assert len(tb.analyses) == 0
    assert tb.options.temp == 27


def test_testbench_auto_name_from_filename():
    """Test Testbench auto-detects name from caller's filename."""
    tb = Testbench()
    # Called from test_testbench.py, should auto-detect "test_testbench"
    assert tb.name == "test_testbench"


def test_testbench_auto_name_datetime_fallback():
    """Test Testbench falls back to datetime when filename detection fails."""
    # Mock _get_caller_filename to return None (simulates REPL/Jupyter)
    with patch('analogpy.testbench._get_caller_filename', return_value=None):
        tb = Testbench()
        # Should match pattern: testbench_YYYYMMDD_HHMMSS
        assert re.match(r'testbench_\d{8}_\d{6}', tb.name)


def test_testbench_with_dut():
    """Test Testbench with instantiated DUT."""
    inv = make_inverter()

    tb = Testbench("tb_inverter")
    vin = tb.net("vin")
    vout = tb.net("vout")
    tb.add_instance(vsource, instance_name="I_vdd", p=tb.net("vdd"), n=tb.gnd(), dc=1.8)
    vdd = tb.net("vdd")
    gnd = tb.gnd()  # Global ground "0" at testbench level

    tb.add_instance(inv, "X1",
                   inp=vin, out=vout, vdd=vdd, vss=gnd)

    assert len(tb.subcircuit_instances) == 2 # Changed from 1 to 2


def test_dc_analysis():
    """Test DC analysis."""
    dc = DC()
    assert dc.name == "dcOp"

    # Default: no maxiters/maxsteps/write/annotate emitted (use Spectre defaults)
    spectre_str = dc.to_spectre()
    assert "dcOp dc" in spectre_str
    assert "maxiters" not in spectre_str
    assert "write=" not in spectre_str
    assert "annotate" not in spectre_str

    # Explicit: parameters emitted when set
    dc2 = DC(maxiters=300, write="spectre.dc", annotate="status")
    spectre_str2 = dc2.to_spectre()
    assert "maxiters=300" in spectre_str2
    assert 'write="spectre.dc"' in spectre_str2
    assert "annotate=status" in spectre_str2


def test_ac_analysis():
    """Test AC analysis."""
    ac = AC(start=1, stop=1e9, points=100)
    spectre_str = ac.to_spectre()

    assert "ac start=1" in spectre_str
    assert "stop=1000000000.0" in spectre_str
    assert "dec=100" in spectre_str


def test_transient_analysis():
    """Test Transient analysis."""
    tran = Transient(stop=1e-6)
    spectre_str = tran.to_spectre()

    assert "tran tran" in spectre_str
    assert "stop=1e-06" in spectre_str


def test_testbench_with_analyses():
    """Test Testbench with multiple analyses."""
    inv = make_inverter()

    tb = Testbench("tb_inverter")
    tb.add_instance(vsource, instance_name="I_vdd", p=tb.net("vdd"), n=tb.gnd(), dc=1.8)
    vdd = tb.net("vdd")
    gnd = tb.gnd()  # Global ground "0" at testbench level
    vin = tb.net("vin")
    vout = tb.net("vout")

    tb.add_instance(inv, "X1",
                   inp=vin, out=vout, vdd=vdd, vss=gnd)

    # Add analyses
    tb.add_analysis(DC())
    tb.add_analysis(AC(start=1, stop=1e9))
    tb.add_analysis(Transient(stop=1e-6))

    assert len(tb.analyses) == 3

    netlist = generate_spectre(tb)
    print("\n=== Testbench with Analyses ===")
    print(netlist)

    # Verify content
    assert "simulator lang=spectre" in netlist
    assert "global 0" in netlist
    assert "simulatorOptions options" in netlist
    assert "dcOp dc" in netlist
    assert "ac ac" in netlist
    assert "tran tran" in netlist


def test_testbench_options():
    """Test Testbench simulator options."""
    tb = Testbench("tb_test")
    tb.set_options(temp=85, reltol=1e-4)
    tb.set_temp(-40)

    assert tb.options.temp == -40
    assert tb.options.reltol == 1e-4


def test_behavioral_model():
    """Test adding behavioral models."""
    tb = Testbench("tb_test")
    tb.behavioral_model("/path/to/model.va")
    tb.behavioral_model("/path/to/another.va")

    assert len(tb.behavioral_models) == 2

    netlist = generate_spectre(tb)
    assert 'ahdl_include "/path/to/model.va"' in netlist
    assert 'ahdl_include "/path/to/another.va"' in netlist


def test_ngspice_analysis():
    """Test ngspice output for analyses."""
    dc = DC()
    ac = AC(start=1, stop=1e9, points=100)
    tran = Transient(stop=1e-6)

    assert dc.to_ngspice() == ".op"
    assert "dec" in ac.to_ngspice()
    assert ".tran" in tran.to_ngspice()


def test_complete_testbench():
    """Test complete testbench generation."""
    inv = make_inverter()

    tb = Testbench("tb_inv_complete")

    # Setup
    tb.add_instance(vsource, instance_name="I_vdd", p=tb.net("vdd"), n=tb.gnd(), dc=1.8)
    vdd = tb.net("vdd")
    gnd = tb.gnd()  # Global ground "0" at testbench level
    vin = tb.net("vin")
    vout = tb.net("vout")

    # DUT
    tb.add_instance(inv, "X_INV",
                   inp=vin, out=vout, vdd=vdd, vss=gnd)

    # Options
    tb.set_temp(27)
    tb.set_options(reltol=1e-3, vabstol=1e-6)

    # Analyses
    tb.add_analysis(DC(save_op=True))
    tb.add_analysis(AC(start=1, stop=10e9, points=200, variation="dec"))
    tb.add_analysis(Transient(stop=100e-9))

    netlist = generate_spectre(tb)
    print("\n=== Complete Testbench ===")
    print(netlist)

    # Comprehensive checks
    assert "tb_inv_complete" in netlist
    assert "subckt inverter" in netlist
    assert "X_INV (vin vout vdd 0) inverter" in netlist
    assert "I_vdd (vdd 0) vsource dc=1.8" in netlist # Changed from "Vvdd (vdd 0) vsource dc=1.8"
    assert "simulatorOptions options" in netlist
    assert "dcOp dc" in netlist
    assert "ac ac" in netlist
    assert "tran tran" in netlist


def test_pss_analysis():
    """Test PSS analysis default and parameterized output."""
    pss = PSS()
    assert pss.name == "myPss"
    assert "myPss pss" == pss.to_spectre()

    pss2 = PSS(fund=1e9, harms=7, tstab=100e-9)
    s = pss2.to_spectre()
    assert "fund=1000000000.0" in s
    assert "harms=7" in s
    assert "tstab=1e-07" in s

    assert "not directly supported" in pss.to_ngspice()


def test_pac_analysis():
    """Test PAC analysis."""
    pac = PAC(stop=100e6)
    s = pac.to_spectre()
    assert "myPac pac" in s
    assert "stop=100000000.0" in s
    assert "not directly supported" in pac.to_ngspice()


def test_pnoise_analysis():
    """Test PNoise analysis."""
    pnoise = PNoise(start=1, stop=100e6, points=200, oprobe="vout")
    s = pnoise.to_spectre()
    assert "myPnoise pnoise" in s
    assert "start=1" in s
    assert "stop=100000000.0" in s
    assert "dec=200" in s
    assert "oprobe=vout" in s
    assert "not directly supported" in pnoise.to_ngspice()


def test_pxf_analysis():
    """Test PXF analysis."""
    pxf = PXF(stop=100e6, oprobe="vout")
    s = pxf.to_spectre()
    assert "myPxf pxf" in s
    assert "stop=100000000.0" in s
    assert "oprobe=vout" in s
    assert "not directly supported" in pxf.to_ngspice()


def test_sp_analysis():
    """Test SP analysis."""
    sp = SP(start=1e6, stop=10e9, points=200)
    s = sp.to_spectre()
    assert "mySp sp" in s
    assert "start=1000000.0" in s
    assert "stop=10000000000.0" in s
    assert "dec=200" in s
    assert "not directly supported" in sp.to_ngspice()


def test_xf_analysis():
    """Test XF analysis."""
    xf = XF(start=1, stop=10e9, oprobe="vout")
    s = xf.to_spectre()
    assert "myXf xf" in s
    assert "start=1" in s
    assert "stop=10000000000.0" in s
    assert "oprobe=vout" in s
    assert "not directly supported" in xf.to_ngspice()


def test_pz_analysis():
    """Test PZ analysis."""
    pz = PZ()
    assert "myPz pz" == pz.to_spectre()

    pz2 = PZ(iprobe="VIN", oprobe="vout")
    s = pz2.to_spectre()
    assert "iprobe=VIN" in s
    assert "oprobe=vout" in s
    assert "not directly supported" in pz.to_ngspice()


def test_sweep_analysis():
    """Test Sweep analysis with block syntax."""
    sweep = Sweep(param="res", start=100, stop=10e3, step=100,
                  children=[DC()])
    s = sweep.to_spectre()
    assert "mySweep sweep" in s
    assert "param=res" in s
    assert "start=100" in s
    assert "stop=10000.0" in s
    assert "step=100" in s
    assert "{" in s
    assert "dcOp dc" in s
    assert "}" in s
    assert "not directly supported" in sweep.to_ngspice()


def test_sweep_with_values():
    """Test Sweep analysis with values list."""
    sweep = Sweep(param="cap", values=[1e-12, 10e-12, 100e-12],
                  children=[AC(start=1, stop=1e9)])
    s = sweep.to_spectre()
    assert "values=[" in s
    assert "1e-12" in s
    assert "ac ac" in s


def test_hb_analysis():
    """Test HB analysis."""
    hb = HB(funds=[1e9], maxharms=[7])
    s = hb.to_spectre()
    assert "myHb hb" in s
    assert "funds=[1000000000.0]" in s
    assert "maxharms=[7]" in s
    assert "not directly supported" in hb.to_ngspice()


def test_dcmatch_analysis():
    """Test DCMatch analysis."""
    dcmatch = DCMatch()
    assert "myDcmatch dcmatch" == dcmatch.to_spectre()

    dcmatch2 = DCMatch(oprobe="vout")
    assert "oprobe=vout" in dcmatch2.to_spectre()
    assert "not directly supported" in dcmatch.to_ngspice()


def test_acmatch_analysis():
    """Test ACMatch analysis."""
    acmatch = ACMatch(start=1, stop=1e9, points=200)
    s = acmatch.to_spectre()
    assert "myAcmatch acmatch" in s
    assert "start=1" in s
    assert "stop=1000000000.0" in s
    assert "dec=200" in s
    assert "not directly supported" in acmatch.to_ngspice()


def test_convenience_methods():
    """Test all convenience methods on Testbench."""
    tb = Testbench("tb_conv")

    tb.pss(fund=1e9, harms=7)
    tb.pac(stop=100e6)
    tb.pnoise(stop=100e6)
    tb.pxf(stop=100e6)
    tb.sp(start=1e6, stop=10e9)
    tb.xf(start=1, stop=10e9)
    tb.pz()
    tb.sweep(param="res", start=100, stop=10e3, step=100, children=[DC()])
    tb.hb(funds=[1e9], maxharms=[7])
    tb.dcmatch()
    tb.acmatch(start=1, stop=1e9)

    assert len(tb.analyses) == 11

    # Verify types
    assert isinstance(tb.analyses[0], PSS)
    assert isinstance(tb.analyses[1], PAC)
    assert isinstance(tb.analyses[2], PNoise)
    assert isinstance(tb.analyses[3], PXF)
    assert isinstance(tb.analyses[4], SP)
    assert isinstance(tb.analyses[5], XF)
    assert isinstance(tb.analyses[6], PZ)
    assert isinstance(tb.analyses[7], Sweep)
    assert isinstance(tb.analyses[8], HB)
    assert isinstance(tb.analyses[9], DCMatch)
    assert isinstance(tb.analyses[10], ACMatch)

    # Verify spectre output for a few
    assert "fund=1000000000.0" in tb.analyses[0].to_spectre()
    assert "harms=7" in tb.analyses[0].to_spectre()
    assert "stop=100000000.0" in tb.analyses[1].to_spectre()


def test_extras_on_new_analyses():
    """Test that extras dict works on new analysis types."""
    pss = PSS(fund=1e9, extras={"saveinit": "yes"})
    s = pss.to_spectre()
    assert "saveinit=yes" in s


if __name__ == "__main__":
    test_testbench_basic()
    print("test_testbench_basic passed")

    test_testbench_auto_name_from_filename()
    print("test_testbench_auto_name_from_filename passed")

    test_testbench_auto_name_datetime_fallback()
    print("test_testbench_auto_name_datetime_fallback passed")

    test_testbench_with_dut()
    print("test_testbench_with_dut passed")

    test_dc_analysis()
    print("test_dc_analysis passed")

    test_ac_analysis()
    print("test_ac_analysis passed")

    test_transient_analysis()
    print("test_transient_analysis passed")

    test_testbench_with_analyses()
    print("test_testbench_with_analyses passed")

    test_testbench_options()
    print("test_testbench_options passed")

    test_behavioral_model()
    print("test_behavioral_model passed")

    test_ngspice_analysis()
    print("test_ngspice_analysis passed")

    test_complete_testbench()
    print("test_complete_testbench passed")

    test_pss_analysis()
    print("test_pss_analysis passed")

    test_pac_analysis()
    print("test_pac_analysis passed")

    test_pnoise_analysis()
    print("test_pnoise_analysis passed")

    test_pxf_analysis()
    print("test_pxf_analysis passed")

    test_sp_analysis()
    print("test_sp_analysis passed")

    test_xf_analysis()
    print("test_xf_analysis passed")

    test_pz_analysis()
    print("test_pz_analysis passed")

    test_sweep_analysis()
    print("test_sweep_analysis passed")

    test_sweep_with_values()
    print("test_sweep_with_values passed")

    test_hb_analysis()
    print("test_hb_analysis passed")

    test_dcmatch_analysis()
    print("test_dcmatch_analysis passed")

    test_acmatch_analysis()
    print("test_acmatch_analysis passed")

    test_convenience_methods()
    print("test_convenience_methods passed")

    test_extras_on_new_analyses()
    print("test_extras_on_new_analyses passed")

    print("\nAll testbench tests passed!")
