"""
Pure‑Python unit tests for the ``st_copy`` wrapper.
They do *not* require a Streamlit runtime or browser.
"""

import uuid
from importlib import reload
from pathlib import Path
from types import ModuleType
from typing import Any
from typing import Dict
from typing import List

import pytest


# Helpers
class _Recorder:
    """Collect calls made to a monkey‑patched ``declare_component``."""

    def __init__(self) -> None:
        self.calls: List[Dict[str, Any]] = []

    def __call__(self, *args, **kwargs):
        # store positional + keyword arguments for later assertions
        self.calls.append({'args': args, **kwargs})

        def _fake_component(**inner_kwargs):
            # a dummy component that simply returns its kwargs,
            # letting the test inspect the values that reached JS
            return inner_kwargs

        return _fake_component


@pytest.fixture
def st_copy_module(monkeypatch) -> ModuleType:
    """
    Import ``st_copy`` while monkey‑patching
    :pyfunc:`streamlit.components.v1.declare_component`.
    """
    from streamlit import components as comps_pkg  # lazy import to avoid heavy deps

    rec = _Recorder()
    monkeypatch.setattr(comps_pkg.v1, 'declare_component', rec)

    import st_copy  # noqa: E402 – import only *after* the patch
    st_copy = reload(st_copy)  # ensure fresh module state
    st_copy._recorder = rec    # make the recorder accessible to tests
    return st_copy


@pytest.fixture
def st_copy_module_dev(monkeypatch) -> ModuleType:
    """Import ``st_copy`` with the dev server enabled."""
    monkeypatch.setenv('ST_COPY_DEV_SERVER', 'auto')
    from streamlit import components as comps_pkg

    rec = _Recorder()
    monkeypatch.setattr(comps_pkg.v1, 'declare_component', rec)

    import st_copy  # noqa: E402
    st_copy = reload(st_copy)
    st_copy._recorder = rec
    return st_copy


# Tests
def test_declare_component_path(st_copy_module):
    """``declare_component`` must be called once with the correct *path*."""
    rec: _Recorder = st_copy_module._recorder
    assert rec.calls, 'declare_component was never called'
    call = rec.calls[0]

    # first positional arg is the component name
    assert call['args'][0] == 'st_copy'

    path = Path(call['path'])
    assert path.name == 'dist'
    assert path.joinpath('index.html').exists()


@pytest.mark.parametrize(
    'call_kwargs',
    [
        # 1) no optional arguments – rely on defaults
        {},
        # 2) a subset of optional arguments
        {'tooltip': 'Copy this', 'key': 'foo'},
        # 3) *all* public arguments overridden
        {
            'icon': 'st',
            'tooltip': 'Special tip',
            'copied_label': 'Done!',
            'key': 'bar',
        },
    ],
)
def test_forwards_all_arguments(st_copy_module, call_kwargs):
    """
    ``copy_button`` must forward *every* parameter unchanged to the JS side.
    """
    recorded: List[Dict[str, Any]] = []

    # replace the real component with a spy
    st_copy_module.component = lambda **kw: recorded.append(kw) or 'sentinel'

    ret = st_copy_module.copy_button('hello', **call_kwargs)

    assert ret == 'sentinel'
    assert recorded, 'Component was never called'
    forwarded = recorded[0]

    # immutable parameter
    assert forwarded['text'] == 'hello'

    # every kwarg supplied by the test must arrive unchanged
    for k, v in call_kwargs.items():
        assert forwarded[k] == v


@pytest.mark.parametrize('icon_val', ['material_symbols', 'st'])
def test_icon_forwarded(st_copy_module, icon_val):
    """The ``icon`` parameter must pass through unchanged."""
    captured = []
    st_copy_module.component = lambda **kw: captured.append(kw) or kw
    st_copy_module.copy_button('hi', icon=icon_val)
    assert captured[0]['icon'] == icon_val


def test_copied_label_default_and_custom(st_copy_module):
    """Check default and custom ``copied_label`` handling."""
    captured = []
    st_copy_module.component = lambda **kw: captured.append(kw) or kw

    # default value
    st_copy_module.copy_button('hi')
    assert captured[0]['copied_label'] == 'Copied!'
    captured.clear()

    # custom value
    st_copy_module.copy_button('hi', copied_label='✅ Copied')
    assert captured[0]['copied_label'] == '✅ Copied'


def test_key_autogenerated_uuid4(st_copy_module):
    """
    If ``key`` is omitted, the wrapper should generate a valid UUID‑4 string.
    """
    captured = []
    st_copy_module.component = lambda **kw: captured.append(kw) or kw
    st_copy_module.copy_button('hi')  # omit ``key``

    auto_key = captured[0]['key']
    # Will raise ValueError if not a valid UUID‑4
    uuid_obj = uuid.UUID(auto_key, version=4)
    assert str(uuid_obj) == auto_key


def test_returns_true_on_success(st_copy_module):
    """``copy_button`` must return ``True`` when the component does."""
    st_copy_module.component = lambda **_: True

    assert st_copy_module.copy_button('hi') is True


def test_declare_component_url(st_copy_module_dev):
    """``declare_component`` must use the development URL when configured."""
    rec: _Recorder = st_copy_module_dev._recorder
    call = rec.calls[0]

    assert call['args'][0] == 'st_copy'
    assert call['url'] == 'http://localhost:3001'

