# 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 the visualization system: symbol generation and collision avoidance."""

import warnings

import pytest

from analogpy import Circuit, nmos, pmos
from analogpy.visualization import (
    create_cell_symbol_standalone,
    drawing_to_svg,
    render_schematic_svg,
)


# ---------------------------------------------------------------------------
# Circuit fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def inverter():
    """CMOS inverter with 4 ports."""
    ckt = Circuit(name="inverter", ports=["inp:input", "out:output", "vdd", "vss"])
    ckt.add_instance(pmos, "MP", d=ckt.net("out"), g=ckt.net("inp"),
                     s=ckt.net("vdd"), b=ckt.net("vdd"), w=2e-6, l=180e-9,
                     schematic_position={'relative_to': "MN", 'x_shift': 0, 'y_shift': 2.1})
    ckt.add_instance(nmos, "MN", d=ckt.net("out"), g=ckt.net("inp"),
                     s=ckt.net("vss"), b=ckt.net("vss"), w=1e-6, l=180e-9)
    ckt.draw_wires('out')
    ckt.draw_wires('inp')
    ckt.draw_wires('vdd', only=[('MP', 's'), ('MP', 'b')])
    ckt.draw_wires('vss', only=[('MN', 's'), ('MN', 'b')])
    return ckt


@pytest.fixture
def dense_cell():
    """Dense cell: 2 inputs, 2 outputs, 3 power, 3 ground."""
    return Circuit(
        name="dense_cell",
        ports=[
            "inp:input", "inn:input",
            "outp:output", "outn:output",
            "vdda", "vdd", "vdda3v3",
            "gnda", "gndd", "sub",
        ],
    )


@pytest.fixture
def adc():
    """ADC with long port names for stress-testing collision avoidance."""
    return Circuit(
        name="adc",
        ports=[
            "inp:input", "inn:input", "ds_adc_ena_vdda3v3:input",
            "outp:output", "outn:output", "loooooooooong_output:output",
            "vdda3v3", "vddd", "vdda1v8", "loooooooong_vdd",
            "gndd", "gnda", "sub", "loooooong_substrate",
        ],
    )


# ---------------------------------------------------------------------------
# Symbol view tests
# ---------------------------------------------------------------------------

class TestSymbolGeneration:
    """Test that symbol SVGs are generated correctly."""

    def test_inverter_symbol_generates(self, inverter):
        drawing = create_cell_symbol_standalone(cell=inverter)
        svg = drawing_to_svg(drawing)
        assert svg.startswith("<svg")
        assert "inverter" in svg

    def test_dense_cell_symbol_generates(self, dense_cell):
        drawing = create_cell_symbol_standalone(cell=dense_cell)
        svg = drawing_to_svg(drawing)
        assert svg.startswith("<svg")
        assert "dense_cell" in svg

    def test_adc_symbol_generates(self, adc):
        drawing = create_cell_symbol_standalone(cell=adc)
        svg = drawing_to_svg(drawing)
        assert svg.startswith("<svg")
        assert "adc" in svg

    def test_positional_cell_arg(self, inverter):
        """cell can be passed as first positional argument."""
        drawing = create_cell_symbol_standalone(inverter)
        svg = drawing_to_svg(drawing)
        assert "inverter" in svg

    def test_standalone_without_circuit(self):
        """Symbol can be created from raw cell_name + ports."""
        drawing = create_cell_symbol_standalone(
            cell_name="oled_cell", ports=["ANODE", "ELVSS"]
        )
        svg = drawing_to_svg(drawing)
        assert "oled_cell" in svg


class TestSymbolCollisionAvoidance:
    """Test that the iterative collision detection resolves all label overlaps."""

    @staticmethod
    def _collect_collision_warnings(cell, **kwargs):
        """Generate a symbol and return any collision/residual warnings."""
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            drawing = create_cell_symbol_standalone(cell=cell, **kwargs)
            drawing_to_svg(drawing)
        return [x for x in w if "collision" in str(x.message).lower()
                or "residual" in str(x.message).lower()]

    def test_inverter_no_collisions(self, inverter):
        cw = self._collect_collision_warnings(inverter)
        assert len(cw) == 0, f"Unexpected collisions: {[str(x.message) for x in cw]}"

    def test_dense_cell_no_collisions(self, dense_cell):
        cw = self._collect_collision_warnings(dense_cell)
        assert len(cw) == 0, f"Unexpected collisions: {[str(x.message) for x in cw]}"

    def test_adc_no_collisions(self, adc):
        cw = self._collect_collision_warnings(adc)
        assert len(cw) == 0, f"Unexpected collisions: {[str(x.message) for x in cw]}"

    def test_show_bboxes_no_collisions(self, adc):
        """show_bboxes=True should not introduce collisions."""
        cw = self._collect_collision_warnings(adc, show_bboxes=True)
        assert len(cw) == 0, f"Unexpected collisions: {[str(x.message) for x in cw]}"


class TestSymbolPortPlacement:
    """Test that ports end up on the correct side of the box."""

    @staticmethod
    def _get_port_positions(cell):
        from analogpy.visualization.symbols import draw_cell_symbol
        import schemdraw
        d = schemdraw.Drawing(show=False)
        pd = getattr(cell, 'port_directions', {})
        return draw_cell_symbol(d, cell.name, cell.ports, port_directions=pd)

    def test_inverter_input_left_output_right(self, inverter):
        pos = self._get_port_positions(inverter)
        # inp (input) should be to the left of out (output)
        assert pos['inp'][0] < pos['out'][0]

    def test_inverter_power_top_ground_bottom(self, inverter):
        pos = self._get_port_positions(inverter)
        # vdd (power) should be above vss (ground)
        assert pos['vdd'][1] > pos['vss'][1]

    def test_adc_inputs_left(self, adc):
        pos = self._get_port_positions(adc)
        center_x = 0
        for name in ['inp', 'inn', 'ds_adc_ena_vdda3v3']:
            assert pos[name][0] < center_x, f"{name} should be on the left"

    def test_adc_outputs_right(self, adc):
        pos = self._get_port_positions(adc)
        center_x = 0
        for name in ['outp', 'outn', 'loooooooooong_output']:
            assert pos[name][0] > center_x, f"{name} should be on the right"

    def test_adc_power_top_ground_bottom(self, adc):
        pos = self._get_port_positions(adc)
        for pwr in ['vdda3v3', 'vddd', 'vdda1v8', 'loooooooong_vdd']:
            for gnd in ['gndd', 'gnda', 'sub', 'loooooong_substrate']:
                assert pos[pwr][1] > pos[gnd][1], f"{pwr} should be above {gnd}"


# ---------------------------------------------------------------------------
# Schematic view tests
# ---------------------------------------------------------------------------

class TestSchematicGeneration:
    """Test that schematic SVGs are generated correctly."""

    def test_inverter_schematic_generates(self, inverter):
        svg = render_schematic_svg(cell=inverter)
        assert svg.startswith("<svg")
        assert len(svg) > 1000  # non-trivial SVG output

    def test_inverter_schematic_with_bboxes(self, inverter):
        svg = render_schematic_svg(cell=inverter, show_bboxes=True)
        assert svg.startswith("<svg")

    def test_inverter_schematic_collision_avoidance(self, inverter):
        """Schematic collision avoidance should resolve all overlaps."""
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            render_schematic_svg(cell=inverter)
        collision_after = [x for x in w if "after avoidance" in str(x.message).lower()
                           and "0 collision" not in str(x.message).lower()]
        assert len(collision_after) == 0
