import tempfile
from unittest.mock import patch

import httpx
import huggingface_hub
import pytest
from huggingface_hub.hf_api import SpaceInfo

import gradio as gr
from gradio.themes.utils import ThemeAsset, get_matching_version, get_theme_assets

versions = [
    "0.1.0",
    "0.1.1",
    "0.1.2",
    "0.1.3",
    "0.4.4",
    "0.5.0",
    "0.7.0",
    "0.9.2",
    "0.9.3",
    "0.9.4",
    "0.9.5",
    "0.9.6",
    "0.9.7",
    "0.9.8",
    "2.2.0",
    "2.2.1",
    "2.2.10",
    "2.2.11",
    "2.2.12",
    "2.2.13",
    "2.2.14",
    "2.2.15",
    "2.2.2",
    "2.2.3",
    "2.2.4",
    "2.2.5",
    "2.2.6",
    "2.2.7",
    "2.2.8",
    "3.0.1",
    "3.0.10",
    "3.0.11",
    "3.0.12",
    "3.0.13",
    "3.0.14",
    "3.0.15",
    "3.0.16",
    "3.0.17",
    "3.0.18",
    "3.0.19",
    "3.0.2",
    "3.0.20",
    "3.0.20-dev0",
    "3.0.21",
    "3.0.22",
    "3.0.23",
    "3.0.23-dev1",
    "3.0.24",
    "3.0.25",
    "3.0.26",
    "3.0.3",
    "3.0.4",
    "3.0.5",
    "3.0.6",
    "3.0.7",
    "3.0.8",
    "3.0.9",
    "3.1.0",
    "3.1.1",
    "3.1.2",
    "3.1.3",
    "3.1.4",
    "3.1.5",
    "3.1.6",
    "3.1.7",
    "3.10.0",
    "3.10.1",
    "3.11.0",
    "3.12.0",
    "3.13.0",
    "3.13.1",
    "3.13.2",
    "3.14.0",
    "3.15.0",
    "3.16.0",
    "3.16.1",
    "3.16.2",
    "3.17.0",
    "3.17.1",
    "3.18.0",
    "3.18.1",
    "3.18.1-dev0",
    "3.18.2-dev0",
    "3.18.2",
    "3.19.0",
    "3.19.1",
    "3.20.0",
    "3.20.1",
    "3.3.1",
    "3.4.1",
    "3.8.1",
    "3.8.2",
    "3.9.1",
]


assets = [ThemeAsset(f"theme_schema@{version}") for version in versions]

dracula_gray = gr.themes.colors.Color(
    *(
        ["#6272a4", "#7280ad", "#818eb6", "#919cbf", "#a1aac8"][::-1]
        + ["#586794", "#4e5b83", "#455073", "#3b4462", "#313952", "#272e42"]
    )
)


dracula_pink = gr.themes.Color(
    *(
        [
            "#ffd7ee",
            "#ff79c6",
            "#ff86cc",
            "#ff94d1",
            "#ffa1d7",
            "#ffafdd",
            "#ffbce3",
            "#ffc9e8",
        ][::-1]
        + ["#e66db2", "#cc619e", "#b3558b"]
    )
)

dracula_gray = gr.themes.colors.Color(
    *(
        ["#6272a4", "#7280ad", "#818eb6", "#919cbf", "#a1aac8"][::-1]
        + ["#586794", "#4e5b83", "#455073", "#3b4462", "#313952", "#272e42"]
    )
)

dracula = gr.themes.Base(
    primary_hue=gr.themes.colors.pink,
    neutral_hue=dracula_gray,
    font=gr.themes.GoogleFont("Poppins"),
).set(
    body_background_fill=dracula_gray.c500,
    color_accent_soft=dracula_gray.c100,
    error_border_color="#fecaca",
    error_background_fill="#fee2e2",
    error_icon_color="#b91c1c",
    error_icon_color_dark="#ef4444",
    error_text_color="#ef4444",
    error_text_color_dark="#ef4444",
    background_fill_primary=dracula_gray.c500,
    background_fill_secondary=dracula_gray.c500,
    block_background_fill=dracula_gray.c300,
    body_text_color="#f8f8f2",
    body_text_color_dark="#f8f8f2",
    body_text_color_subdued="#f8f8f2",
    block_label_text_color="#f8f8f2",
    block_label_text_color_dark="#f8f8f2",
    table_even_background_fill=dracula_gray.c300,
    border_color_accent=dracula_gray.c200,
    block_info_text_color="#f8f8f2",
    block_info_text_color_dark="#f8f8f2",
    block_title_text_color="#f8f8f2",
    block_title_text_color_dark="#f8f8f2",
    checkbox_background_color_selected_dark="#ff79c6",
    checkbox_background_color_selected="#ff79c6",
    button_primary_background_fill_dark="#ff79c6",
    button_primary_background_fill=dracula_pink.c300,
    button_secondary_text_color="#f8f8f2",
    slider_color_dark="#ff79c6",
    slider_color=dracula_pink.c300,
    panel_background_fill="#31395294",
    block_background_fill_dark="#31395294",
)
dracula.name = "gradio/dracula_test"


class TestSemverMatch:
    def test_simple_equality(self):
        assert get_matching_version(assets, "3.10.0") == ThemeAsset(
            "theme_schema@3.10.0"
        )

    def test_empty_expression_returns_latest(self):
        assert get_matching_version(assets, None) == ThemeAsset("theme_schema@3.20.1")

    def test_range(self):
        assert get_matching_version(assets, ">=3.10.0,<3.15") == ThemeAsset(
            "theme_schema@3.14.0"
        )

    def test_wildcard(self):
        assert get_matching_version(assets, "2.2.*") == ThemeAsset(
            "theme_schema@2.2.15"
        )

    def test_does_not_exist(self):
        assert get_matching_version(assets, ">4.0.0") is None

    def test_compatible_release_specifier(self):
        assert get_matching_version(assets, "~=0.0") == ThemeAsset("theme_schema@0.9.8")

    def test_breaks_ties_against_prerelease(self):
        assert get_matching_version(assets, ">=3.18,<3.19") == ThemeAsset(
            "theme_schema@3.18.2"
        )


class TestGetThemeAssets:
    def test_get_theme_assets(self):
        space_info = huggingface_hub.hf_api.SpaceInfo(
            id="freddyaboulton/dracula",
            siblings=[
                {
                    "blob_id": None,
                    "lfs": None,
                    "rfilename": "themes/theme_schema@0.1.0.json",
                    "size": None,
                },
                {
                    "blob_id": None,
                    "lfs": None,
                    "rfilename": "themes/theme_schema@0.1.1.json",
                    "size": None,
                },
                {
                    "blob_id": None,
                    "lfs": None,
                    "rfilename": "themes/theme_schema@0.2.5.json",
                    "size": None,
                },
                {
                    "blob_id": None,
                    "lfs": None,
                    "rfilename": "themes/theme_schema@1.5.9.json",
                    "size": None,
                },
            ],
            tags=["gradio-theme", "gradio"],
            private=False,
            likes=0,
        )

        assert get_theme_assets(space_info) == [
            ThemeAsset("themes/theme_schema@0.1.0.json"),
            ThemeAsset("themes/theme_schema@0.1.1.json"),
            ThemeAsset("themes/theme_schema@0.2.5.json"),
            ThemeAsset("themes/theme_schema@1.5.9.json"),
        ]

        assert gr.Theme._theme_version_exists(space_info, "0.1.1")
        assert not gr.Theme._theme_version_exists(space_info, "2.0.0")

    @pytest.mark.flaky
    def test_load_space_from_hub_works(self):
        theme = gr.Theme.from_hub("gradio/seafoam")
        assert isinstance(theme, gr.Theme)


class TestBuiltInThemes:
    @pytest.mark.parametrize(
        "theme, name",
        [
            (gr.themes.Base(), "base"),
            (gr.themes.Glass(), "glass"),
            (gr.themes.Monochrome(), "monochrome"),
            (gr.themes.Soft(), "soft"),
            (gr.themes.Default(), "default"),
        ],
    )
    def test_theme_name(self, theme, name):
        assert theme.name == name


class TestThemeUploadDownload:
    @patch("gradio.themes.base.get_theme_assets", return_value=assets)
    def test_get_next_version(self, mock):
        next_version = gr.themes.Base._get_next_version(
            SpaceInfo(id="gradio/dracula_test", private=False, likes=0, tags=[])
        )
        assert next_version == "3.20.2"

    ## Commenting out until after 4.0 Spaces are up
    # @pytest.mark.flaky
    # def test_theme_download(self):
    #     assert (
    #         gr.themes.Base.from_hub("gradio/dracula_test@0.0.2").to_dict()
    #         == dracula.to_dict()
    #     )

    #     with gr.Blocks(theme="gradio/dracula_test@0.0.2") as demo:
    #         pass

    #     assert demo.theme.to_dict() == dracula.to_dict()
    #     assert demo.theme.name == "gradio/dracula_test"

    @pytest.mark.flaky
    def test_theme_download_raises_error_if_theme_does_not_exist(self):
        with pytest.raises(
            ValueError, match="The space freddyaboulton/nonexistent does not exist"
        ):
            gr.themes.Base.from_hub("freddyaboulton/nonexistent").to_dict()

    @patch("gradio.themes.base.huggingface_hub")
    @patch("gradio.themes.base.Base._theme_version_exists", return_value=True)
    def test_theme_upload_fails_if_duplicate_version(self, mock_1, mock_2):
        with pytest.raises(ValueError, match="already has a theme with version 0.2.1"):
            dracula.push_to_hub("dracula_revamped", version="0.2.1", token="foo")

    @patch("gradio.themes.base.huggingface_hub")
    @patch("gradio.themes.base.huggingface_hub.HfApi")
    def test_upload_fails_if_not_valid_semver(self, mock_1, mock_2):
        with pytest.raises(ValueError, match="Invalid version string: '3.0'"):
            dracula.push_to_hub("dracula_revamped", version="3.0", token="s")

    def test_dump_and_load(self):
        with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as path:
            dracula.dump(path.name)
            assert gr.themes.Base.load(path.name).to_dict() == dracula.to_dict()

    @patch("gradio.themes.base.Base._get_next_version", return_value="0.1.3")
    @patch("gradio.themes.base.Base._theme_version_exists", return_value=False)
    @patch("gradio.themes.base.huggingface_hub")
    def test_version_and_token_optional(self, mock_1, mock_2, mock_3):
        mock_1.whoami.return_value = {"name": "freddyaboulton"}

        gr.themes.Monochrome().push_to_hub(repo_name="my_monochrome")
        repo_call_args = mock_1.HfApi().create_commit.call_args_list[0][1]
        assert repo_call_args["repo_id"] == "freddyaboulton/my_monochrome"
        assert any(
            o.path_in_repo == "themes/theme_schema@0.1.3.json"
            for o in repo_call_args["operations"]
        )
        mock_1.whoami.assert_called_with()

    @patch("gradio.themes.base.huggingface_hub")
    def test_first_upload_no_version(self, mock_1):
        mock_1.whoami.return_value = {"name": "freddyaboulton"}

        mock_1.HfApi().space_info.side_effect = httpx.HTTPError("Foo")  # type: ignore

        gr.themes.Monochrome().push_to_hub(repo_name="does_not_exist")
        repo_call_args = mock_1.HfApi().create_commit.call_args_list[0][1]
        assert repo_call_args["repo_id"] == "freddyaboulton/does_not_exist"
        assert any(
            o.path_in_repo == "themes/theme_schema@0.0.1.json"
            for o in repo_call_args["operations"]
        )
        mock_1.whoami.assert_called_with()

    @patch("gradio.themes.base.Base._get_next_version", return_value="0.1.3")
    @patch("gradio.themes.base.Base._theme_version_exists", return_value=False)
    @patch("gradio.themes.base.huggingface_hub")
    def test_can_pass_version_and_theme(self, mock_1, mock_2, mock_3):
        mock_1.whoami.return_value = {"name": "freddyaboulton"}

        gr.themes.Monochrome().push_to_hub(
            repo_name="my_monochrome", version="0.1.5", token="foo"
        )
        repo_call_args = mock_1.HfApi().create_commit.call_args_list[0][1]
        assert repo_call_args["repo_id"] == "freddyaboulton/my_monochrome"
        assert any(
            o.path_in_repo == "themes/theme_schema@0.1.5.json"
            for o in repo_call_args["operations"]
        )
        mock_1.whoami.assert_called_with(token="foo")

    @patch("gradio.themes.base.huggingface_hub")
    def test_raise_error_if_no_token_and_not_logged_in(self, mock_1):
        mock_1.whoami.side_effect = OSError("not logged in")

        with pytest.raises(
            ValueError,
            match="In order to push to hub, log in via `huggingface-cli login`",
        ):
            gr.themes.Monochrome().push_to_hub(
                repo_name="my_monochrome", version="0.1.5"
            )

    @patch("gradio.themes.base.Base._get_next_version", return_value="0.1.3")
    @patch("gradio.themes.base.Base._theme_version_exists", return_value=False)
    @patch("gradio.themes.base.huggingface_hub")
    def test_can_upload_to_org(self, mock_1, mock_2, mock_3):
        mock_1.whoami.return_value = {"name": "freddyaboulton"}

        gr.themes.Monochrome().push_to_hub(
            repo_name="my_monochrome", version="0.1.9", org_name="gradio"
        )
        repo_call_args = mock_1.HfApi().create_commit.call_args_list[0][1]
        assert repo_call_args["repo_id"] == "gradio/my_monochrome"
        assert any(
            o.path_in_repo == "themes/theme_schema@0.1.9.json"
            for o in repo_call_args["operations"]
        )
        mock_1.whoami.assert_called_with()

    @patch("gradio.themes.base.Base._get_next_version", return_value="0.1.3")
    @patch("gradio.themes.base.Base._theme_version_exists", return_value=False)
    @patch("gradio.themes.base.huggingface_hub")
    def test_can_make_private(self, mock_1, mock_2, mock_3):
        mock_1.whoami.return_value = {"name": "freddyaboulton"}

        gr.themes.Monochrome().push_to_hub(
            repo_name="my_monochrome", version="0.1.9", org_name="gradio", private=True
        )
        mock_1.create_repo.assert_called_with(
            "gradio/my_monochrome",
            repo_type="space",
            space_sdk="gradio",
            token=None,
            exist_ok=True,
            private=True,
        )


@pytest.mark.serial
def test_theme_builder_launches():
    gr.themes.builder(prevent_thread_lock=True)
    gr.close_all()
