import pytest
import yaml

from huggingface_hub import SpaceCardData
from huggingface_hub.repocard_data import (
    CardData,
    DatasetCardData,
    EvalResult,
    ModelCardData,
    eval_results_to_model_index,
    model_index_to_eval_results,
)


OPEN_LLM_LEADERBOARD_URL = "https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard"
DUMMY_METADATA_WITH_MODEL_INDEX = """
language: en
license: mit
library_name: timm
tags:
- pytorch
- image-classification
datasets:
- beans
metrics:
- acc
model-index:
- name: my-cool-model
  results:
  - task:
      type: image-classification
    dataset:
      type: beans
      name: Beans
    metrics:
    - type: acc
      value: 0.9
    source:
      name: Open LLM Leaderboard
      url: https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard
"""


class TestBaseCardData:
    def test_metadata_behave_as_dict(self):
        metadata = CardData(foo="bar")

        # .get and __getitem__
        assert metadata.get("foo") == "bar"
        assert metadata.get("FOO") is None  # case-sensitive
        assert metadata["foo"] == "bar"
        with pytest.raises(KeyError):  # case-sensitive
            _ = metadata["FOO"]

        # __setitem__
        metadata["foo"] = "BAR"
        assert metadata.get("foo") == "BAR"
        assert metadata["foo"] == "BAR"

        # __contains__
        assert "foo" in metadata
        assert "FOO" not in metadata

        # default value
        # Should return default when key is not in metadata
        assert metadata.get("FOO", "default") == "default"
        # Should return default when key is in metadata but value is None
        metadata.FOO = None
        assert metadata.get("FOO", "default") == "default"
        # export
        assert str(metadata) == "foo: BAR"

        # .pop
        assert metadata.pop("foo") == "BAR"


class TestModelCardData:
    def test_eval_results_to_model_index(self):
        expected_results = yaml.safe_load(DUMMY_METADATA_WITH_MODEL_INDEX)

        eval_results = [
            EvalResult(
                task_type="image-classification",
                dataset_type="beans",
                dataset_name="Beans",
                metric_type="acc",
                metric_value=0.9,
                source_name="Open LLM Leaderboard",
                source_url=OPEN_LLM_LEADERBOARD_URL,
            ),
        ]

        model_index = eval_results_to_model_index("my-cool-model", eval_results)

        assert model_index == expected_results["model-index"]

    def test_model_index_to_eval_results(self):
        model_index = [
            {
                "name": "my-cool-model",
                "results": [
                    {
                        "task": {
                            "type": "image-classification",
                        },
                        "dataset": {
                            "type": "cats_vs_dogs",
                            "name": "Cats vs. Dogs",
                        },
                        "metrics": [
                            {
                                "type": "acc",
                                "value": 0.85,
                            },
                            {
                                "type": "f1",
                                "value": 0.9,
                            },
                        ],
                    },
                    {
                        "task": {
                            "type": "image-classification",
                        },
                        "dataset": {
                            "type": "beans",
                            "name": "Beans",
                        },
                        "metrics": [
                            {
                                "type": "acc",
                                "value": 0.9,
                                "verified": True,
                                "verifyToken": 1234,
                            }
                        ],
                        "source": {
                            "name": "Open LLM Leaderboard",
                            "url": OPEN_LLM_LEADERBOARD_URL,
                        },
                    },
                ],
            }
        ]
        model_name, eval_results = model_index_to_eval_results(model_index)

        assert len(eval_results) == 3
        assert model_name == "my-cool-model"

        assert eval_results[0].dataset_type == "cats_vs_dogs"
        assert eval_results[0].source_name is None
        assert eval_results[0].source_url is None

        assert eval_results[1].metric_type == "f1"
        assert eval_results[1].metric_value == 0.9
        assert eval_results[1].source_name is None
        assert eval_results[1].source_url is None

        assert eval_results[2].task_type == "image-classification"
        assert eval_results[2].dataset_type == "beans"
        assert eval_results[2].verified is True
        assert eval_results[2].verify_token == 1234
        assert eval_results[2].source_name == "Open LLM Leaderboard"
        assert eval_results[2].source_url == OPEN_LLM_LEADERBOARD_URL

    def test_card_data_requires_model_name_for_eval_results(self):
        with pytest.raises(ValueError, match="`eval_results` requires `model_name` to be set."):
            ModelCardData(
                eval_results=[
                    EvalResult(
                        task_type="image-classification",
                        dataset_type="beans",
                        dataset_name="Beans",
                        metric_type="acc",
                        metric_value=0.9,
                    ),
                ],
            )

        data = ModelCardData(
            model_name="my-cool-model",
            eval_results=[
                EvalResult(
                    task_type="image-classification",
                    dataset_type="beans",
                    dataset_name="Beans",
                    metric_type="acc",
                    metric_value=0.9,
                ),
            ],
        )

        model_index = eval_results_to_model_index(data.model_name, data.eval_results)

        assert model_index[0]["name"] == "my-cool-model"
        assert model_index[0]["results"][0]["task"]["type"] == "image-classification"

    def test_arbitrary_incoming_card_data(self):
        data = ModelCardData(
            model_name="my-cool-model",
            eval_results=[
                EvalResult(
                    task_type="image-classification",
                    dataset_type="beans",
                    dataset_name="Beans",
                    metric_type="acc",
                    metric_value=0.9,
                ),
            ],
            some_arbitrary_kwarg="some_value",
        )

        assert data.some_arbitrary_kwarg == "some_value"

        data_dict = data.to_dict()
        assert data_dict["some_arbitrary_kwarg"] == "some_value"

    def test_eval_result_with_incomplete_source(self):
        # Source url without name: ok
        EvalResult(
            task_type="image-classification",
            dataset_type="beans",
            dataset_name="Beans",
            metric_type="acc",
            metric_value=0.9,
            source_url=OPEN_LLM_LEADERBOARD_URL,
        )

        # Source name without url: not ok
        with pytest.raises(ValueError):
            EvalResult(
                task_type="image-classification",
                dataset_type="beans",
                dataset_name="Beans",
                metric_type="acc",
                metric_value=0.9,
                source_name="Open LLM Leaderboard",
            )

    def test_model_card_unique_tags(self):
        data = ModelCardData(tags=["tag2", "tag1", "tag2", "tag3"])
        assert data.tags == ["tag2", "tag1", "tag3"]

    def test_remove_top_level_none_values(self):
        as_obj = ModelCardData(tags=["tag1", None], foo={"bar": 3, "baz": None}, pipeline_tag=None)
        as_dict = as_obj.to_dict()

        assert as_obj.tags == ["tag1", None]
        assert as_dict["tags"] == ["tag1", None]  # none value inside list should be kept

        assert as_obj.foo == {"bar": 3, "baz": None}
        assert as_dict["foo"] == {"bar": 3, "baz": None}  # none value inside dict should be kept

        assert as_obj.pipeline_tag is None
        assert "pipeline_tag" not in as_dict  # top level none value should be removed

    def test_eval_results_requires_evalresult_type(self):
        with pytest.raises(ValueError, match="should be of type `EvalResult` or a list of `EvalResult`"):
            ModelCardData(model_name="my-cool-model", eval_results="this is not an EvalResult")

        with pytest.raises(ValueError, match="should be of type `EvalResult` or a list of `EvalResult`"):
            ModelCardData(model_name="my-cool-model", eval_results=["accuracy: 0.9", "f1: 0.85"])

        data = ModelCardData(
            model_name="my-cool-model",
            eval_results="this is not an EvalResult",
            ignore_metadata_errors=True,
        )
        assert data.eval_results is not None and data.eval_results == "this is not an EvalResult"

    def test_model_name_required_with_eval_results(self):
        with pytest.raises(ValueError, match="`eval_results` requires `model_name` to be set"):
            ModelCardData(
                eval_results=[
                    EvalResult(
                        task_type="image-classification",
                        dataset_type="beans",
                        dataset_name="Beans",
                        metric_type="acc",
                        metric_value=0.9,
                    ),
                ],
            )

        eval_results = [
            EvalResult(
                task_type="image-classification",
                dataset_type="beans",
                dataset_name="Beans",
                metric_type="acc",
                metric_value=0.9,
            ),
        ]
        data = ModelCardData(
            eval_results=eval_results,
            ignore_metadata_errors=True,
        )
        assert data.eval_results is not None and data.eval_results == eval_results


class TestDatasetCardData:
    def test_train_eval_index_keys_updated(self):
        train_eval_index = [
            {
                "config": "plain_text",
                "task": "text-classification",
                "task_id": "binary_classification",
                "splits": {"train_split": "train", "eval_split": "test"},
                "col_mapping": {"text": "text", "label": "target"},
                "metrics": [
                    {
                        "type": "accuracy",
                        "name": "Accuracy",
                    },
                    {"type": "f1", "name": "F1 macro", "args": {"average": "macro"}},
                ],
            }
        ]
        card_data = DatasetCardData(
            language="en",
            license="mit",
            pretty_name="My Cool Dataset",
            train_eval_index=train_eval_index,
        )
        # The init should have popped this out of kwargs and into train_eval_index attr
        assert card_data.train_eval_index == train_eval_index
        # Underlying train_eval_index gets converted to train-eval-index in DatasetCardData._to_dict.
        # So train_eval_index should be None in the dict
        assert card_data.to_dict().get("train_eval_index") is None
        # And train-eval-index should be in the dict
        assert card_data.to_dict()["train-eval-index"] == train_eval_index


class TestSpaceCardData:
    def test_space_card_data(self) -> None:
        card_data = SpaceCardData(
            title="Dreambooth Training",
            license="mit",
            sdk="gradio",
            duplicated_from="multimodalart/dreambooth-training",
        )
        assert card_data.to_dict() == {
            "title": "Dreambooth Training",
            "sdk": "gradio",
            "license": "mit",
            "duplicated_from": "multimodalart/dreambooth-training",
        }
        assert card_data.tags is None  # SpaceCardData has some default attributes
