from __future__ import annotations
from typing import cast, List, Sequence

import pytest
from rich.console import RenderableType
from rich.text import Text

from tests.utilities.test_app import AppTest
from textual.app import ComposeResult
from textual.css.types import EdgeType
from textual.geometry import Size
from textual.widget import Widget
from textual.widgets import Placeholder

pytestmark = pytest.mark.integration_test

# Let's allow ourselves some abbreviated names for those tests,
# in order to make the test cases a bit easier to read :-)
SCREEN_W = 100  # width of our Screens
SCREEN_H = 8  # height of our Screens
SCREEN_SIZE = Size(SCREEN_W, SCREEN_H)
PLACEHOLDERS_DEFAULT_H = 3  # the default height for our Placeholder widgets

# As per Widget's CSS property, by default Widgets have a horizontal scrollbar of size 1
# and a vertical scrollbar of size 2:
SCROLL_H_SIZE = 1
SCROLL_V_SIZE = 2


@pytest.mark.asyncio
@pytest.mark.parametrize(
    (
        "placeholders_count",
        "root_container_style",
        "placeholders_style",
        "expected_root_widget_virtual_size",
        "expected_placeholders_size",
        "expected_placeholders_offset_x",
    ),
    (
        *[
            [
                1,
                f"border: {invisible_border_edge};",  # #root has no visible border
                "",  # no specific placeholder style
                # #root's virtual size=screen size
                (SCREEN_W, SCREEN_H),
                # placeholders width=same than screen :: height=default height
                (SCREEN_W, PLACEHOLDERS_DEFAULT_H),
                # placeholders should be at offset 0
                0,
            ]
            for invisible_border_edge in ("", "none", "hidden")
        ],
        [
            1,
            "border: solid white;",  # #root has a visible border
            "",  # no specific placeholder style
            # #root's virtual size is smaller because of its borders
            (SCREEN_W - 2, SCREEN_H - 2),
            # placeholders width=same than screen, minus 2 borders :: height=default height minus 2 borders
            (SCREEN_W - 2, PLACEHOLDERS_DEFAULT_H),
            # placeholders should be at offset 1 because of #root's border
            1,
        ],
        [
            4,
            "border: solid white;",  # #root has a visible border
            "",  # no specific placeholder style
            # #root's virtual height should be as high as its stacked content
            (SCREEN_W - 2 - 1, PLACEHOLDERS_DEFAULT_H * 4),
            # placeholders width=same than screen, minus 2 borders, minus scrollbar :: height=default height minus 2 borders
            (SCREEN_W - 2 - SCROLL_V_SIZE, PLACEHOLDERS_DEFAULT_H),
            # placeholders should be at offset 1 because of #root's border
            1,
        ],
        [
            1,
            "border: solid white;",  # #root has a visible border
            "align: center top;",  # placeholders are centered horizontally
            # #root's virtual size=screen size
            (SCREEN_W, SCREEN_H),
            # placeholders width=same than screen, minus 2 borders :: height=default height
            (SCREEN_W - 2, PLACEHOLDERS_DEFAULT_H),
            # placeholders should be at offset 1 because of #root's border
            1,
        ],
        [
            4,
            "border: solid white;",  # #root has a visible border
            "align: center top;",  # placeholders are centered horizontally
            # #root's virtual height should be as high as its stacked content
            (
                SCREEN_W - 2 - SCROLL_V_SIZE,
                PLACEHOLDERS_DEFAULT_H * 4,
            ),
            # placeholders width=same than screen, minus 2 borders, minus scrollbar :: height=default height
            (SCREEN_W - 2 - SCROLL_V_SIZE, PLACEHOLDERS_DEFAULT_H),
            # placeholders should be at offset 1 because of #root's border
            1,
        ],
    ),
)
async def test_composition_of_vertical_container_with_children(
    placeholders_count: int,
    root_container_style: str,
    placeholders_style: str,
    expected_placeholders_size: tuple[int, int],
    expected_root_widget_virtual_size: tuple[int, int],
    expected_placeholders_offset_x: int,
):
    class VerticalContainer(Widget):
        DEFAULT_CSS = (
            """
        VerticalContainer {
            layout: vertical;
            overflow: hidden auto;
            ${root_container_style}
        }

        VerticalContainer Placeholder {
            height: ${placeholders_height};
            ${placeholders_style}
        }
        """.replace(
                "${root_container_style}", root_container_style
            )
            .replace("${placeholders_height}", str(PLACEHOLDERS_DEFAULT_H))
            .replace("${placeholders_style}", placeholders_style)
        )

    class MyTestApp(AppTest):
        def compose(self) -> ComposeResult:
            placeholders = [
                Placeholder(id=f"placeholder_{i}", name=f"Placeholder #{i}")
                for i in range(placeholders_count)
            ]

            yield VerticalContainer(*placeholders, id="root")

    app = MyTestApp(size=SCREEN_SIZE, test_name="compositor")

    expected_screen_size = SCREEN_SIZE

    async with app.in_running_state():
        # root widget checks:
        root_widget = cast(Widget, app.get_child("root"))
        assert root_widget.outer_size == expected_screen_size
        root_widget_region = app.screen.find_widget(root_widget).region
        assert root_widget_region == (
            0,
            0,
            expected_screen_size.width,
            expected_screen_size.height,
        )

        app_placeholders = cast(List[Widget], app.query("Placeholder"))
        assert len(app_placeholders) == placeholders_count

    # placeholder widgets checks:
    for placeholder in app_placeholders:
        assert placeholder.outer_size == expected_placeholders_size
        assert placeholder.styles.offset.x.value == 0.0
        assert app.screen.get_offset(placeholder).x == expected_placeholders_offset_x


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "edge_type,expected_box_inner_size,expected_box_size,expected_top_left_edge_color,expects_visible_char_at_top_left_edge",
    (
        # These first 3 types of border edge types are synonyms, and display no borders:
        ["", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
        ["none", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
        ["hidden", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
        # Let's transition to "blank": we still see no visible border, but the size is increased
        # as the gutter space is reserved the same way it would be with a border:
        ["blank", Size(SCREEN_W - 2, 1), Size(SCREEN_W, 3), "#ffffff", False],
        # And now for the "normally visible" border edge types:
        # --> we see a visible border, and the size is increased:
        *[
            [edge_style, Size(SCREEN_W - 2, 1), Size(SCREEN_W, 3), "#ffffff", True]
            for edge_style in [
                "round",
                "solid",
                "double",
                "dashed",
                "heavy",
                "inner",
                "outer",
                "hkey",
                "vkey",
            ]
        ],
    ),
)
async def test_border_edge_types_impact_on_widget_size(
    edge_type: EdgeType,
    expected_box_inner_size: Size,
    expected_box_size: Size,
    expected_top_left_edge_color: str,
    expects_visible_char_at_top_left_edge: bool,
):
    class BorderTarget(Widget):
        def render(self) -> RenderableType:
            return Text("border target", style="black on yellow", justify="center")

    border_target = BorderTarget()
    border_target.styles.height = "auto"
    border_target.styles.border = (edge_type, "white")

    class MyTestApp(AppTest):
        def compose(self) -> ComposeResult:
            yield border_target

    app = MyTestApp(size=SCREEN_SIZE, test_name="border_edge_types")

    await app.boot_and_shutdown()

    box_inner_size = Size(
        border_target.content_region.width,
        border_target.content_region.height,
    )
    assert box_inner_size == expected_box_inner_size

    assert border_target.outer_size == expected_box_size

    top_left_edge_style = app.screen.get_style_at(0, 0)
    top_left_edge_color = top_left_edge_style.color.name
    assert top_left_edge_color.upper() == expected_top_left_edge_color.upper()

    top_left_edge_char = app.get_char_at(0, 0)
    top_left_edge_char_is_a_visible_one = top_left_edge_char != " "
    assert top_left_edge_char_is_a_visible_one == expects_visible_char_at_top_left_edge


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "large_widget_size,container_style,expected_large_widget_visible_region_size",
    (
        # In these tests we're going to insert a "large widget"
        # into a container with size (20,20).
        # ---------------- let's start!
        # no overflow/scrollbar instructions: no scrollbars
        [Size(30, 30), "color: red", Size(20, 20)],
        # explicit hiding of the overflow: no scrollbars either
        [Size(30, 30), "overflow: hidden", Size(20, 20)],
        # scrollbar for both directions
        [
            Size(30, 30),
            "overflow: auto",
            Size(
                20 - SCROLL_V_SIZE,
                20 - SCROLL_H_SIZE,
            ),
        ],
        # horizontal scrollbar
        [Size(30, 30), "overflow-x: auto", Size(20, 20 - SCROLL_H_SIZE)],
        # vertical scrollbar
        [Size(30, 30), "overflow-y: auto", Size(20 - SCROLL_V_SIZE, 20)],
        # scrollbar for both directions, custom scrollbar size
        [Size(30, 30), ("overflow: auto", "scrollbar-size: 3 5"), Size(20 - 5, 20 - 3)],
        # scrollbar for both directions, custom vertical scrollbar size
        [
            Size(30, 30),
            ("overflow: auto", "scrollbar-size-vertical: 3"),
            Size(20 - 3, 20 - SCROLL_H_SIZE),
        ],
        # scrollbar for both directions, custom horizontal scrollbar size
        [
            Size(30, 30),
            ("overflow: auto", "scrollbar-size-horizontal: 3"),
            Size(20 - SCROLL_V_SIZE, 20 - 3),
        ],
        # scrollbar needed only vertically, custom scrollbar size
        [
            Size(20, 30),
            ("overflow: auto", "scrollbar-size: 3 3"),
            Size(20 - 3, 20),
        ],
        # scrollbar needed only horizontally, custom scrollbar size
        [
            Size(30, 20),
            ("overflow: auto", "scrollbar-size: 3 3"),
            Size(20, 20 - 3),
        ],
    ),
)
async def test_scrollbar_size_impact_on_the_layout(
    large_widget_size: Size,
    container_style: str | Sequence[str],
    expected_large_widget_visible_region_size: Size,
):
    class LargeWidget(Widget):
        def on_mount(self):
            self.styles.width = large_widget_size[0]
            self.styles.height = large_widget_size[1]

    container_style_rules = (
        [container_style] if isinstance(container_style, str) else container_style
    )

    class LargeWidgetContainer(Widget):
        # TODO: Once textual#581 ("Default versus User CSS") is solved the following CSS should just use the
        #  "LargeWidgetContainer" selector, without having to use a more specific one to be able to override Widget's CSS:
        DEFAULT_CSS = """
        #large-widget-container {
            width: 20;
            height: 20;
            ${container_style};
        }
        """.replace(
            "${container_style}",
            ";\n".join(container_style_rules),
        )

    large_widget = LargeWidget()
    container = LargeWidgetContainer(large_widget, id="large-widget-container")

    class MyTestApp(AppTest):
        def compose(self) -> ComposeResult:
            yield container

    app = MyTestApp(size=Size(40, 40), test_name="scrollbar_size_impact_on_the_layout")

    await app.boot_and_shutdown()

    compositor = app.screen._compositor
    widgets_map = compositor.map
    large_widget_visible_region_size = widgets_map[large_widget].visible_region.size
    assert large_widget_visible_region_size == expected_large_widget_visible_region_size
