from contextlib import ExitStack as DoesNotRaise
from typing import List, TypeVar, Optional, Tuple, Dict

import pytest

from supervision import Detections
from supervision.dataset.utils import train_test_split, merge_class_lists, build_class_index_mapping, \
    map_detections_class_id
from test.utils import mock_detections

T = TypeVar("T")


@pytest.mark.parametrize(
    'data, train_ratio, random_state, shuffle, expected_result, exception',
    [
        (
            [],
            0.5,
            None,
            False,
            ([], []),
            DoesNotRaise()
        ),  # empty data
        (
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
            0.5,
            None,
            False,
            ([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]),
            DoesNotRaise()
        ),  # data with 10 numbers and 50% train split
        (
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
            1.0,
            None,
            False,
            ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], []),
            DoesNotRaise()
        ),  # data with 10 numbers and 100% train split
        (
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
            0.0,
            None,
            False,
            ([], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
            DoesNotRaise()
        ),  # data with 10 numbers and 0% train split
        (
            ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'],
            0.5,
            None,
            False,
            (['a', 'b', 'c', 'd', 'e'], ['f', 'g', 'h', 'i', 'j']),
            DoesNotRaise()
        ),  # data with 10 chars and 50% train split
        (
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
            0.5,
            23,
            True,
            ([7, 8, 5, 6, 3], [2, 9, 0, 1, 4]),
            DoesNotRaise()
        ),  # data with 10 numbers and 50% train split with 23 random seed
        (
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
            0.5,
            32,
            True,
            ([4, 6, 0, 8, 9], [5, 7, 2, 3, 1]),
            DoesNotRaise()
        ),  # data with 10 numbers and 50% train split with 23 random seed
    ]
)
def test_train_test_split(
    data: List[T],
    train_ratio: float,
    random_state: int,
    shuffle: bool,
    expected_result: Optional[Tuple[List[T], List[T]]],
    exception: Exception
) -> None:
    with exception:
        result = train_test_split(data=data, train_ratio=train_ratio, random_state=random_state, shuffle=shuffle)
        assert result == expected_result


@pytest.mark.parametrize(
    'class_lists, expected_result, exception',
    [
        (
            [],
            [],
            DoesNotRaise()
        ),  # empty class lists
        (
            [
                ['dog', 'person']
            ],
            ['dog', 'person'],
            DoesNotRaise()
        ),  # single class list; already alphabetically sorted
        (
            [
                ['person', 'dog']
            ],
            ['dog', 'person'],
            DoesNotRaise()
        ),  # single class list; not alphabetically sorted
        (
            [
                ['dog', 'person'],
                ['dog', 'person']
            ],
            ['dog', 'person'],
            DoesNotRaise()
        ),  # two class lists; the same classes; already alphabetically sorted
        (
            [
                ['dog', 'person'],
                ['cat']
            ],
            ['cat', 'dog', 'person'],
            DoesNotRaise()
        ),  # two class lists; different classes; already alphabetically sorted
    ]
)
def test_merge_class_maps(class_lists: List[List[str]], expected_result: List[str], exception: Exception) -> None:
    with exception:
        result = merge_class_lists(class_lists=class_lists)
        assert result == expected_result


@pytest.mark.parametrize(
    'source_classes, target_classes, expected_result, exception',
    [
        (
            [],
            [],
            {},
            DoesNotRaise()
        ),  # empty class lists
        (
            [],
            ['dog', 'person'],
            {},
            DoesNotRaise()
        ),  # empty source class list
        (
            ['dog', 'person'],
            [],
            None,
            pytest.raises(ValueError)
        ),  # empty target class list
        (
            ['dog', 'person'],
            ['dog', 'person'],
            {0: 0, 1: 1},
            DoesNotRaise()
        ),  # same class lists
        (
            ['dog', 'person'],
            ['person', 'dog'],
            {0: 1, 1: 0},
            DoesNotRaise()
        ),  # same class lists but not alphabetically sorted
        (
            ['dog', 'person'],
            ['cat', 'dog', 'person'],
            {0: 1, 1: 2},
            DoesNotRaise()
        ),  # source class list is a subset of target class list
        (
            ['dog', 'person'],
            ['cat', 'dog'],
            None,
            pytest.raises(ValueError)
        ),  # source class list is not a subset of target class list
    ]
)
def test_build_class_index_mapping(
    source_classes: List[str],
    target_classes: List[str],
    expected_result: Optional[Dict[int, int]],
    exception: Exception
) -> None:
    with exception:
        result = build_class_index_mapping(source_classes=source_classes, target_classes=target_classes)
        assert result == expected_result


@pytest.mark.parametrize(
    'source_to_target_mapping, detections, expected_result, exception',
    [
        (
            {},
            mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
            None,
            pytest.raises(ValueError)
        ),  # empty mapping
        (
            {0: 1},
            mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
            mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
            DoesNotRaise()
        ),  # single mapping
        (
            {0: 1, 1: 2},
            mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
            mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
            DoesNotRaise()
        ),  # multiple mappings
        (
            {0: 1, 1: 2},
            mock_detections(xyxy=[[0, 0, 10, 10], [0, 0, 10, 10]], class_id=[0, 1]),
            mock_detections(xyxy=[[0, 0, 10, 10], [0, 0, 10, 10]], class_id=[1, 2]),
            DoesNotRaise()
        ),  # multiple mappings
        (
            {0: 1, 1: 2},
            mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[2]),
            None,
            pytest.raises(ValueError)
        ),  # class_id not in mapping
        (
            {0: 1, 1: 2},
            mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0], confidence=[0.5]),
            mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1], confidence=[0.5]),
            DoesNotRaise(),
        ),  # confidence is not None
    ]
)
def test_map_detections_class_id(
    source_to_target_mapping: Dict[int, int],
    detections: Detections,
    expected_result: Optional[Detections],
    exception: Exception
) -> None:
    with exception:
        result = map_detections_class_id(source_to_target_mapping=source_to_target_mapping, detections=detections)
        assert result == expected_result
