import io
import sys
from types import ModuleType

import pytest

from rich import inspect
from rich.console import Console

skip_py36 = pytest.mark.skipif(
    sys.version_info.minor == 6 and sys.version_info.major == 3,
    reason="rendered differently on py3.6",
)

skip_py37 = pytest.mark.skipif(
    sys.version_info.minor == 7 and sys.version_info.major == 3,
    reason="rendered differently on py3.7",
)

skip_py38 = pytest.mark.skipif(
    sys.version_info.minor == 8 and sys.version_info.major == 3,
    reason="rendered differently on py3.8",
)

skip_py39 = pytest.mark.skipif(
    sys.version_info.minor == 9 and sys.version_info.major == 3,
    reason="rendered differently on py3.9",
)

skip_py310 = pytest.mark.skipif(
    sys.version_info.minor == 10 and sys.version_info.major == 3,
    reason="rendered differently on py3.10",
)


def render(obj, methods=False, value=False, width=50) -> str:
    console = Console(file=io.StringIO(), width=width, legacy_windows=False)
    inspect(obj, console=console, methods=methods, value=value)
    return console.file.getvalue()


class InspectError(Exception):
    def __str__(self) -> str:
        return "INSPECT ERROR"


class Foo:
    """Foo test

    Second line
    """

    def __init__(self, foo: int) -> None:
        """constructor docs."""
        self.foo = foo

    @property
    def broken(self):
        raise InspectError()

    def method(self, a, b) -> str:
        """Multi line

        docs.
        """
        return "test"

    def __dir__(self):
        return ["__init__", "broken", "method"]


@skip_py36
def test_render():
    console = Console(width=100, file=io.StringIO(), legacy_windows=False)

    foo = Foo("hello")
    inspect(foo, console=console, all=True, value=False)
    result = console.file.getvalue()
    print(repr(result))
    expected = "╭────────────── <class 'tests.test_inspect.Foo'> ──────────────╮\n│ Foo test                                                     │\n│                                                              │\n│   broken = InspectError()                                    │\n│ __init__ = def __init__(foo: int) -> None: constructor docs. │\n│   method = def method(a, b) -> str: Multi line               │\n╰──────────────────────────────────────────────────────────────╯\n"
    assert expected == result


def test_inspect_text():
    expected = (
        "╭──────────────── <class 'str'> ─────────────────╮\n"
        "│ str(object='') -> str                          │\n"
        "│ str(bytes_or_buffer[, encoding[, errors]]) ->  │\n"
        "│ str                                            │\n"
        "│                                                │\n"
        "│ 33 attribute(s) not shown. Run                 │\n"
        "│ inspect(inspect) for options.                  │\n"
        "╰────────────────────────────────────────────────╯\n"
    )
    print(repr(expected))
    assert expected == render("Hello")


@skip_py36
@skip_py37
def test_inspect_empty_dict():
    expected = (
        "╭──────────────── <class 'dict'> ────────────────╮\n"
        "│ dict() -> new empty dictionary                 │\n"
        "│ dict(mapping) -> new dictionary initialized    │\n"
        "│ from a mapping object's                        │\n"
        "│     (key, value) pairs                         │\n"
        "│ dict(iterable) -> new dictionary initialized   │\n"
        "│ as if via:                                     │\n"
        "│     d = {}                                     │\n"
        "│     for k, v in iterable:                      │\n"
        "│         d[k] = v                               │\n"
        "│ dict(**kwargs) -> new dictionary initialized   │\n"
        "│ with the name=value pairs                      │\n"
        "│     in the keyword argument list.  For         │\n"
        "│ example:  dict(one=1, two=2)                   │\n"
        "│                                                │\n"
    )
    assert render({}).startswith(expected)


def test_inspect_builtin_function():
    expected = (
        "╭────────── <built-in function print> ───────────╮\n"
        "│ def print(...)                                 │\n"
        "│                                                │\n"
        "│ print(value, ..., sep=' ', end='\\n',           │\n"
        "│ file=sys.stdout, flush=False)                  │\n"
        "│                                                │\n"
        "│ 29 attribute(s) not shown. Run                 │\n"
        "│ inspect(inspect) for options.                  │\n"
        "╰────────────────────────────────────────────────╯\n"
    )
    assert expected == render(print)


@skip_py36
def test_inspect_integer():
    expected = (
        "╭────── <class 'int'> ───────╮\n"
        "│ int([x]) -> integer        │\n"
        "│ int(x, base=10) -> integer │\n"
        "│                            │\n"
        "│ denominator = 1            │\n"
        "│        imag = 0            │\n"
        "│   numerator = 1            │\n"
        "│        real = 1            │\n"
        "╰────────────────────────────╯\n"
    )
    assert expected == render(1)


@skip_py36
def test_inspect_integer_with_value():
    expected = "╭────── <class 'int'> ───────╮\n│ int([x]) -> integer        │\n│ int(x, base=10) -> integer │\n│                            │\n│ ╭────────────────────────╮ │\n│ │ 1                      │ │\n│ ╰────────────────────────╯ │\n│                            │\n│ denominator = 1            │\n│        imag = 0            │\n│   numerator = 1            │\n│        real = 1            │\n╰────────────────────────────╯\n"
    value = render(1, value=True)
    print(repr(value))
    assert expected == value


@skip_py36
@skip_py37
@skip_py310
def test_inspect_integer_with_methods():
    expected = (
        "╭──────────────── <class 'int'> ─────────────────╮\n"
        "│ int([x]) -> integer                            │\n"
        "│ int(x, base=10) -> integer                     │\n"
        "│                                                │\n"
        "│      denominator = 1                           │\n"
        "│             imag = 0                           │\n"
        "│        numerator = 1                           │\n"
        "│             real = 1                           │\n"
        "│ as_integer_ratio = def as_integer_ratio():     │\n"
        "│                    Return integer ratio.       │\n"
        "│       bit_length = def bit_length(): Number of │\n"
        "│                    bits necessary to represent │\n"
        "│                    self in binary.             │\n"
        "│        conjugate = def conjugate(...) Returns  │\n"
        "│                    self, the complex conjugate │\n"
        "│                    of any int.                 │\n"
        "│       from_bytes = def from_bytes(bytes,       │\n"
        "│                    byteorder, *,               │\n"
        "│                    signed=False): Return the   │\n"
        "│                    integer represented by the  │\n"
        "│                    given array of bytes.       │\n"
        "│         to_bytes = def to_bytes(length,        │\n"
        "│                    byteorder, *,               │\n"
        "│                    signed=False): Return an    │\n"
        "│                    array of bytes representing │\n"
        "│                    an integer.                 │\n"
        "╰────────────────────────────────────────────────╯\n"
    )
    assert expected == render(1, methods=True)


@skip_py36
@skip_py37
@skip_py38
@skip_py39
def test_inspect_integer_with_methods():
    expected = (
        "╭──────────────── <class 'int'> ─────────────────╮\n"
        "│ int([x]) -> integer                            │\n"
        "│ int(x, base=10) -> integer                     │\n"
        "│                                                │\n"
        "│      denominator = 1                           │\n"
        "│             imag = 0                           │\n"
        "│        numerator = 1                           │\n"
        "│             real = 1                           │\n"
        "│ as_integer_ratio = def as_integer_ratio():     │\n"
        "│                    Return integer ratio.       │\n"
        "│        bit_count = def bit_count(): Number of  │\n"
        "│                    ones in the binary          │\n"
        "│                    representation of the       │\n"
        "│                    absolute value of self.     │\n"
        "│       bit_length = def bit_length(): Number of │\n"
        "│                    bits necessary to represent │\n"
        "│                    self in binary.             │\n"
        "│        conjugate = def conjugate(...) Returns  │\n"
        "│                    self, the complex conjugate │\n"
        "│                    of any int.                 │\n"
        "│       from_bytes = def from_bytes(bytes,       │\n"
        "│                    byteorder, *,               │\n"
        "│                    signed=False): Return the   │\n"
        "│                    integer represented by the  │\n"
        "│                    given array of bytes.       │\n"
        "│         to_bytes = def to_bytes(length,        │\n"
        "│                    byteorder, *,               │\n"
        "│                    signed=False): Return an    │\n"
        "│                    array of bytes representing │\n"
        "│                    an integer.                 │\n"
        "╰────────────────────────────────────────────────╯\n"
    )
    assert expected == render(1, methods=True)


@skip_py36
@skip_py37
def test_broken_call_attr():
    class NotCallable:
        __call__ = 5  # Passes callable() but isn't really callable

        def __repr__(self):
            return "NotCallable()"

    class Foo:
        foo = NotCallable()

    foo = Foo()
    assert callable(foo.foo)
    expected = "╭─ <class 'tests.test_inspect.test_broken_call_attr.<locals>.Foo'> ─╮\n│ foo = NotCallable()                                               │\n╰───────────────────────────────────────────────────────────────────╯\n"
    result = render(foo, methods=True, width=100)
    print(repr(result))
    assert expected == result


def test_inspect_swig_edge_case():
    """Issue #1838 - Edge case with Faiss library - object with empty dir()"""

    class Thing:
        @property
        def __class__(self):
            raise AttributeError

    thing = Thing()
    try:
        inspect(thing)
    except Exception as e:
        assert False, f"Object with no __class__ shouldn't raise {e}"


def test_inspect_module_with_class():
    def function():
        pass

    class Thing:
        """Docstring"""

        pass

    module = ModuleType("my_module")
    module.SomeClass = Thing
    module.function = function

    expected = (
        "╭────────── <module 'my_module'> ──────────╮\n"
        "│  function = def function():              │\n"
        "│ SomeClass = class SomeClass(): Docstring │\n"
        "╰──────────────────────────────────────────╯\n"
    )
    assert render(module, methods=True) == expected
