import typing
import dataclasses

import pytest
from rna.pattern.link import LinkNotFoundError, Linker, Link, Reference, exposed


@dataclasses.dataclass
class ExampleReference(Reference):
    path: str = None
    data_1: any = None
    data_2: any = None

    def __post_init__(self):
        super().__init__()


@dataclasses.dataclass
class ExampleLinker(Linker):
    data_1: typing.Union[Link, float] = Link(value=None)
    data_2: typing.Union[Link, float] = Link(value=None)
    flux_surfaces: typing.Union[Link, float] = Link(value=None)
    dummy: any = "any value"


def test_basics():
    linker_explicit = ExampleLinker(flux_surfaces=42)
    assert linker_explicit.flux_surfaces == 42


@pytest.fixture
def ref():
    ref = ExampleReference(path="wout_asdf.nc", data_1=21, data_2=42)
    return ref


@pytest.fixture
def linker(ref):
    linker = ExampleLinker(
        data_1=Link(ref=ref, fget=lambda wo: wo.data_1),
        data_2=Link(ref=ref, fget=lambda wo: wo.data_2),
        flux_surfaces=Link("./flux_surfaces_123.txt -> 123", lambda ref: int(ref[-3:])),
    )
    assert linker.flux_surfaces == 123
    return linker


def test_exposed(linker):
    """
    Linker attributes reveal the ref with the exposed switch set to True
    """
    with exposed(True):
        assert "./flux_surfaces" in linker.flux_surfaces.ref
    assert linker.flux_surfaces == 123


def test_get_link(linker, ref):
    # You can also use the 'get_link' method to do the same
    assert linker.data_2 == 42
    link = linker.get_link("data_2")
    assert link.ref is ref

    # It raises LinkNotFoundError if no Link instance is found
    with pytest.raises(LinkNotFoundError):
        linker.get_link("dummy") is None
    assert linker.dummy == "any value"


def test_get_ref(linker, ref):
    # Or in short use the 'get_ref' method to directly access the reference
    gotten_ref = linker.get_ref("data_2")
    assert gotten_ref is ref

    # You can find all references to the wget ref by the _references attribute.
    # This is only possible, if the referenced object has is an instance of Reference.
    assert isinstance(ref, Reference)

    # The first two entries in _references stam from linker_explicit, the last two from linker
    assert ref._references[0][0] is linker
    assert ref._references[0][1] == "data_1"
    assert ref._references[1][0] is linker
    assert ref._references[1][1] == "data_2"

    # The cache attribute allows to - if true - store the call value of fget (at the expense
    # of not being up to date with the ref). -> Only use the cache=True value if you are
    # sure that the reference object will never change the referred value.
    linker_cached = ExampleLinker(data_2=Link(ref=ref, fget=lambda wo: wo.data_2, cache=True))
    assert linker_cached.data_2 == 42
    ref.data_2 = 12
    assert linker_cached.data_2 == 42
    assert linker.data_2 == 12


def test_get_buggy_fget():
    """ """

    def f_bug(ref):
        raise NotImplementedError("bug")

    @dataclasses.dataclass
    class BuggyLinker(Linker):
        buggy_attr: typing.Union[Link, float] = Link(ref=None, fget=f_bug)

    with pytest.raises(NotImplementedError):
        BuggyLinker().buggy_attr
