import numpy as np
import pytest

from manim.mobject.text.code_mobject import Code
from manim.utils.color.core import ManimColor


def test_code_initialization_from_string():
    code_string = """from manim import Scene, Square

class FadeInSquare(Scene):
    def construct(self):
        s = Square()
        self.play(FadeIn(s))
        self.play(s.animate.scale(2))
        self.wait()"""
    rendered_code = Code(
        code_string=code_string,
        language="python",
    )
    num_lines = len(code_string.split("\n"))
    assert len(rendered_code.code_lines) == num_lines
    assert len(rendered_code.line_numbers) == num_lines


@pytest.mark.parametrize(
    ("code_string", "num_lines"),
    [("pass", 1), ("   ", 1), (" \n ", 2)],
)
def test_code_initialization_without_line_numbers(code_string, num_lines):
    rendered_code = Code(
        code_string=code_string,
        language="python",
        add_line_numbers=False,
    )

    assert len(rendered_code.code_lines) == num_lines
    assert not hasattr(rendered_code, "line_numbers")


def test_code_initialization_from_file():
    rendered_code = Code(
        code_file="example_scenes/basic.py",
        language="python",
        background="window",
        background_config={"fill_color": "#101010"},
        paragraph_config={"font_size": 14},
    )
    assert len(rendered_code.code_lines) == len(rendered_code.line_numbers)
    assert rendered_code.background.fill_color == ManimColor("#101010")


@pytest.mark.parametrize(
    ("formatter_style", "background_color", "text_color"),
    [
        ("xcode", "#ffffff", "#000000"),
        ("vim", "#000000", "#cccccc"),
    ],
)
def test_code_uses_formatter_style_colors(
    formatter_style, background_color, text_color
):
    rendered_code = Code(
        code_string="plain text",
        language="text",
        formatter_style=formatter_style,
        add_line_numbers=False,
    )

    assert rendered_code.background.fill_color == ManimColor(background_color)
    assert rendered_code.code_lines[0][0].get_color() == ManimColor(text_color)


def test_code_ignores_paragraph_color():
    rendered_code = Code(
        code_string="plain text",
        language="text",
        formatter_style="vim",
        paragraph_config={"color": "#ff0000"},
    )

    assert rendered_code.code_lines[0][0].get_color() == ManimColor("#cccccc")
    assert rendered_code.line_numbers[0][0].get_color() == ManimColor("#cccccc")


def test_code_ignores_default_paragraph_color(monkeypatch):
    monkeypatch.setitem(Code.default_paragraph_config, "color", "#ff0000")

    rendered_code = Code(
        code_string="plain text",
        language="text",
        formatter_style="vim",
    )

    assert rendered_code.code_lines[0][0].get_color() == ManimColor("#cccccc")
    assert rendered_code.line_numbers[0][0].get_color() == ManimColor("#cccccc")


def test_code_accepts_custom_pygments_style():
    class CustomStyle(Code.get_pygments_style("xcode")):
        background_color = "#123456"
        line_number_color = "#abcdef"

    rendered_code = Code(
        code_string="plain text",
        language="text",
        formatter_style=CustomStyle,
    )

    assert rendered_code.background.fill_color == ManimColor("#123456")
    assert rendered_code.code_lines[0][0].get_color() == ManimColor("#000000")
    assert rendered_code.line_numbers[0][0].get_color() == ManimColor("#abcdef")


@pytest.mark.parametrize(
    ("formatter_style", "line_number_color"),
    [
        ("solarized-light", ManimColor("#93a1a1")),
        ("vim", ManimColor("#cccccc")),
        ("xcode", ManimColor("#000000")),
    ],
)
def test_code_line_numbers_use_formatter_style_color(
    formatter_style, line_number_color
):
    rendered_code = Code(
        code_string="plain text",
        language="text",
        formatter_style=formatter_style,
    )

    assert rendered_code.line_numbers[0][0].get_color() == line_number_color


def test_line_heights_initial_whitespace():
    rendered_code = Code(
        code_string="""print('Hello, World!')
for _ in range(42):
    print('Hello, World!')
""",
        language="python",
    )
    np.testing.assert_almost_equal(
        rendered_code.code_lines[0].height,
        rendered_code.code_lines[2].height,
    )


@pytest.mark.parametrize("line_numbers_from", [1, 10, -2])
def test_code_baseline_alignment(line_numbers_from):
    baseline_offsets = []
    for code_string in ("pass", "pass b"):
        rendered_code = Code(
            code_string=code_string,
            language="python",
            line_numbers_from=line_numbers_from,
        )
        baseline_offsets.append(
            rendered_code.code_lines[0].get_bottom()[1]
            - rendered_code.line_numbers[0].get_bottom()[1]
        )

    np.testing.assert_allclose(baseline_offsets[0], baseline_offsets[1], atol=1e-6)


@pytest.mark.parametrize("background", ["rectangle", "window"])
@pytest.mark.parametrize("add_line_numbers", [True, False])
def test_code_height_is_independent_of_boundary_glyphs(
    background,
    add_line_numbers,
):
    code_strings = (
        "AAA\nppp",
        "ppp\nAAA",
        "AAA\nAAA",
        "ppp\nppp",
        "---\n---",
    )
    background_heights = [
        Code(
            code_string=code_string,
            language="python",
            background=background,
            add_line_numbers=add_line_numbers,
        ).background.height
        for code_string in code_strings
    ]

    np.testing.assert_allclose(background_heights, background_heights[0], atol=1e-6)


@pytest.mark.parametrize("disable_ligatures", [True, False])
@pytest.mark.parametrize("add_line_numbers", [True, False])
def test_code_line_mobject_is_independent_of_ordinality(
    disable_ligatures, add_line_numbers
):
    """Test that the (sub)mobject structure and width of a line of code does not depend
    on the line's order in the block.
    """
    code_strings = ("Hello World!",) * 5
    code_mobject = Code(
        code_string="\n".join(code_strings),
        add_line_numbers=add_line_numbers,
        paragraph_config={"disable_ligatures": disable_ligatures},
    )
    lines = code_mobject.code_lines
    reference_line = lines[len(lines) // 2]
    for boundary_line in (lines[0], lines[-1]):
        assert len(boundary_line) == len(reference_line)
        np.testing.assert_allclose(
            boundary_line.width,
            reference_line.width,
            atol=1e-6,
        )


def test_code_syntax_highlighting_colors():
    code_string = "pass\n# comment"
    rendered_code = Code(
        code_string=code_string,
        language="python",
        formatter_style="vim",
    )

    assert {char.color for char in rendered_code.code_lines[0]} == {
        ManimColor("#CDCD00")
    }
    assert {char.color for char in rendered_code.code_lines[1]} == {
        ManimColor("#000080")
    }
    assert [len(line) for line in rendered_code.code_lines] == [
        sum(not char.isspace() for char in line) for line in code_string.splitlines()
    ]
    assert len(rendered_code.code_lines) == len(rendered_code.code_lines.chars)


def test_code_initialization_style_correct_color():
    for style in Code.get_styles_list():
        try:
            Code(
                code_string="""# This is a comment.
var = 3
print(var)
""",
                formatter_style=style,
            )
        except ValueError as e:
            pytest.fail(f"Code initialization failed for style {style} with error: {e}")
