"""Tests for FromDMToYOLOConverter and helper methods."""

from __future__ import annotations

import json

import pytest

from synapse_sdk.plugins.datasets.converters.yolo.from_dm import (
    FromDMToYOLOConverter,
)

# -----------------------------------------------------------------------------
# Test _get_bbox_data
# -----------------------------------------------------------------------------


class TestGetBboxData:
    """Tests for FromDMToYOLOConverter._get_bbox_data method."""

    @pytest.fixture
    def converter(self, tmp_path):
        """Create converter instance with sample class map."""
        conv = FromDMToYOLOConverter(root_dir=tmp_path)
        conv.class_map = {'car': 0, 'truck': 1}
        return conv

    def test_get_bbox_data_from_data_array(self, converter):
        """Extract bbox from data = [x, y, w, h] format."""
        ann = {'data': [100, 150, 200, 100]}
        result = converter._get_bbox_data(ann)

        assert result == (100, 150, 200, 100)

    def test_get_bbox_data_from_points(self, converter):
        """Extract bbox from points = [[x1, y1], [x2, y2]] format (corners)."""
        ann = {'points': [[100, 150], [300, 250]]}
        result = converter._get_bbox_data(ann)

        # Should convert to (x, y, w, h) format
        assert result == (100, 150, 200, 100)

    def test_get_bbox_data_from_points_reversed_corners(self, converter):
        """Extract bbox from points with reversed corner order."""
        ann = {'points': [[300, 250], [100, 150]]}
        result = converter._get_bbox_data(ann)

        # Should handle reversed corners (min for x/y, abs for w/h)
        assert result == (100, 150, 200, 100)

    def test_get_bbox_data_from_individual_fields(self, converter):
        """Extract bbox from separate x, y, width, height fields."""
        ann = {'x': 50, 'y': 75, 'width': 150, 'height': 200}
        result = converter._get_bbox_data(ann)

        assert result == (50, 75, 150, 200)

    def test_get_bbox_data_from_geometry(self, converter):
        """Extract bbox from geometry = [x, y, w, h] format."""
        ann = {'geometry': [25, 50, 100, 80]}
        result = converter._get_bbox_data(ann)

        assert result == (25, 50, 100, 80)

    def test_get_bbox_data_returns_none_for_missing(self, converter):
        """Return None when no valid bbox format found."""
        ann = {'some_other_field': 'value'}
        result = converter._get_bbox_data(ann)

        assert result is None

    def test_get_bbox_data_handles_short_arrays(self, converter):
        """Handle arrays with < 4 elements gracefully."""
        # Short data array
        ann = {'data': [100, 150]}
        result = converter._get_bbox_data(ann)
        assert result is None

        # Short geometry array
        ann = {'geometry': [100]}
        result = converter._get_bbox_data(ann)
        assert result is None

    def test_get_bbox_data_priority_data_first(self, converter):
        """Data field takes priority over other formats."""
        ann = {
            'data': [10, 20, 30, 40],
            'x': 100,
            'y': 200,
            'width': 300,
            'height': 400,
        }
        result = converter._get_bbox_data(ann)

        # Should use 'data' format
        assert result == (10, 20, 30, 40)


# -----------------------------------------------------------------------------
# Test _get_polygon_data
# -----------------------------------------------------------------------------


class TestGetPolygonData:
    """Tests for FromDMToYOLOConverter._get_polygon_data method."""

    @pytest.fixture
    def converter(self, tmp_path):
        """Create converter instance."""
        conv = FromDMToYOLOConverter(root_dir=tmp_path)
        conv.class_map = {'road': 0}
        return conv

    def test_get_polygon_data_from_data(self, converter):
        """Extract polygon from data = [[x1, y1], ...] format."""
        ann = {'data': [[0, 0], [100, 0], [100, 100], [0, 100]]}
        result = converter._get_polygon_data(ann)

        assert result == [[0, 0], [100, 0], [100, 100], [0, 100]]

    def test_get_polygon_data_from_points(self, converter):
        """Extract polygon from points = [[x1, y1], ...] format."""
        ann = {'points': [[50, 50], [150, 50], [150, 150]]}
        result = converter._get_polygon_data(ann)

        assert result == [[50, 50], [150, 50], [150, 150]]

    def test_get_polygon_data_from_geometry(self, converter):
        """Extract polygon from geometry = [[x1, y1], ...] format."""
        ann = {'geometry': [[10, 20], [30, 40], [50, 60]]}
        result = converter._get_polygon_data(ann)

        assert result == [[10, 20], [30, 40], [50, 60]]

    def test_get_polygon_data_returns_none(self, converter):
        """Return None when no valid polygon format found."""
        ann = {'classification': 'road'}
        result = converter._get_polygon_data(ann)

        assert result is None

    def test_get_polygon_data_priority_data_first(self, converter):
        """Data field takes priority over other formats."""
        ann = {
            'data': [[1, 1], [2, 2], [3, 3]],
            'points': [[10, 10], [20, 20], [30, 30]],
        }
        result = converter._get_polygon_data(ann)

        assert result == [[1, 1], [2, 2], [3, 3]]


# -----------------------------------------------------------------------------
# Test _convert_v2_annotations
# -----------------------------------------------------------------------------


class TestConvertV2Annotations:
    """Tests for FromDMToYOLOConverter._convert_v2_annotations method."""

    def test_convert_bounding_box_to_yolo(self, tmp_path):
        """Correct normalization of bounding box to YOLO format."""
        converter = FromDMToYOLOConverter(root_dir=tmp_path)
        converter.class_map = {'car': 0}

        img_ann = {
            'bounding_box': [
                {
                    'classification': 'car',
                    'x': 100,
                    'y': 150,
                    'width': 200,
                    'height': 100,
                }
            ],
            'polygon': [],
            'keypoint': [],
        }

        lines = converter._convert_v2_annotations(img_ann, width=800, height=600)

        assert len(lines) == 1
        parts = lines[0].split()
        assert parts[0] == '0'  # Class index

        # YOLO format: center_x, center_y, width, height (normalized)
        cx = float(parts[1])
        cy = float(parts[2])
        w = float(parts[3])
        h = float(parts[4])

        # Expected: center=(200, 200), size=(200, 100)
        # Normalized: cx=200/800=0.25, cy=200/600=0.333, w=200/800=0.25, h=100/600=0.167
        assert abs(cx - 0.25) < 0.001
        assert abs(cy - (150 + 50) / 600) < 0.001
        assert abs(w - 0.25) < 0.001
        assert abs(h - 100 / 600) < 0.001

    def test_convert_polygon_to_yolo(self, tmp_path):
        """Polygon coordinate normalization to YOLO segmentation format."""
        converter = FromDMToYOLOConverter(root_dir=tmp_path)
        converter.class_map = {'road': 0}

        img_ann = {
            'bounding_box': [],
            'polygon': [
                {
                    'classification': 'road',
                    'points': [[0, 400], [800, 400], [800, 600], [0, 600]],
                }
            ],
            'keypoint': [],
        }

        lines = converter._convert_v2_annotations(img_ann, width=800, height=600)

        assert len(lines) == 1
        parts = lines[0].split()
        assert parts[0] == '0'  # Class index

        # Remaining parts are normalized polygon coordinates
        # Each point: x/width y/height
        coords = [float(p) for p in parts[1:]]
        assert len(coords) == 8  # 4 points * 2 coords

        # First point: (0, 400) -> (0.0, 0.667)
        assert abs(coords[0] - 0.0) < 0.001
        assert abs(coords[1] - 400 / 600) < 0.001

    def test_convert_skips_unknown_class(self, tmp_path):
        """Skip annotations with classification not in class_map."""
        converter = FromDMToYOLOConverter(root_dir=tmp_path)
        converter.class_map = {'car': 0}  # Only 'car'

        img_ann = {
            'bounding_box': [
                {
                    'classification': 'unknown_class',  # Not in class_map
                    'x': 100,
                    'y': 100,
                    'width': 50,
                    'height': 50,
                },
                {
                    'classification': 'car',  # In class_map
                    'x': 200,
                    'y': 200,
                    'width': 100,
                    'height': 100,
                },
            ],
            'polygon': [],
            'keypoint': [],
        }

        lines = converter._convert_v2_annotations(img_ann, width=800, height=600)

        # Only the 'car' annotation should be included
        assert len(lines) == 1
        assert lines[0].startswith('0 ')  # car class index

    def test_convert_skips_missing_data(self, tmp_path):
        """Skip annotations when _get_bbox_data returns None."""
        converter = FromDMToYOLOConverter(root_dir=tmp_path)
        converter.class_map = {'car': 0}

        img_ann = {
            'bounding_box': [
                {
                    'classification': 'car',
                    # Missing all coordinate data
                }
            ],
            'polygon': [],
            'keypoint': [],
        }

        lines = converter._convert_v2_annotations(img_ann, width=800, height=600)

        assert len(lines) == 0

    def test_convert_multiple_annotations(self, tmp_path):
        """Convert multiple annotations of different types."""
        converter = FromDMToYOLOConverter(root_dir=tmp_path)
        converter.class_map = {'car': 0, 'road': 1}

        img_ann = {
            'bounding_box': [
                {'classification': 'car', 'x': 100, 'y': 100, 'width': 50, 'height': 50},
                {'classification': 'car', 'x': 300, 'y': 200, 'width': 80, 'height': 60},
            ],
            'polygon': [
                {'classification': 'road', 'points': [[0, 0], [100, 0], [100, 100]]},
            ],
            'keypoint': [],
        }

        lines = converter._convert_v2_annotations(img_ann, width=800, height=600)

        # 2 bboxes + 1 polygon
        assert len(lines) == 3
        bbox_lines = [line for line in lines if line.startswith('0 ')]
        polygon_lines = [line for line in lines if line.startswith('1 ')]
        assert len(bbox_lines) == 2
        assert len(polygon_lines) == 1


# -----------------------------------------------------------------------------
# Test get_all_classes
# -----------------------------------------------------------------------------


class TestGetAllClasses:
    """Tests for FromDMToYOLOConverter.get_all_classes static method."""

    def test_get_all_classes_dm_v2(self, temp_multi_class_dir):
        """Collect classes from DMv2 JSON files."""
        from synapse_sdk.plugins.datasets.converters.yolo.from_dm import DMVersion

        classes = FromDMToYOLOConverter.get_all_classes(
            [temp_multi_class_dir],
            dm_version=DMVersion.V2,
        )

        # Should find: car, truck, pedestrian, bicycle
        assert sorted(classes) == ['bicycle', 'car', 'pedestrian', 'truck']

    def test_get_all_classes_dm_v1(self, tmp_path):
        """Collect classes from DMv1 JSON files."""
        from synapse_sdk.plugins.datasets.converters.yolo.from_dm import DMVersion

        json_dir = tmp_path / 'json'
        json_dir.mkdir()

        # Create V1 format JSON
        v1_json = {
            'annotations': {
                'image_1.jpg': [
                    {'classification': {'category': 'dog'}},
                    {'classification': {'category': 'cat'}},
                ],
                'image_2.jpg': [
                    {'classification': {'category': 'dog'}},
                ],
            }
        }
        (json_dir / 'annotations.json').write_text(json.dumps(v1_json))

        classes = FromDMToYOLOConverter.get_all_classes(
            [tmp_path],
            dm_version=DMVersion.V1,
        )

        assert sorted(classes) == ['cat', 'dog']

    def test_get_all_classes_sorted(self, temp_multi_class_dir):
        """Returns sorted unique classes."""
        from synapse_sdk.plugins.datasets.converters.yolo.from_dm import DMVersion

        classes = FromDMToYOLOConverter.get_all_classes(
            [temp_multi_class_dir],
            dm_version=DMVersion.V2,
        )

        # Should be sorted alphabetically
        assert classes == sorted(classes)
        # Should have no duplicates
        assert len(classes) == len(set(classes))

    def test_get_all_classes_empty_dir(self, tmp_path):
        """Handle empty directories gracefully."""
        from synapse_sdk.plugins.datasets.converters.yolo.from_dm import DMVersion

        json_dir = tmp_path / 'json'
        json_dir.mkdir()

        classes = FromDMToYOLOConverter.get_all_classes(
            [tmp_path],
            dm_version=DMVersion.V2,
        )

        assert classes == []

    def test_get_all_classes_multiple_dirs(self, tmp_path):
        """Collect classes from multiple directories."""
        from synapse_sdk.plugins.datasets.converters.yolo.from_dm import DMVersion

        # Create two directories with different classes
        dir1 = tmp_path / 'train'
        dir2 = tmp_path / 'val'
        (dir1 / 'json').mkdir(parents=True)
        (dir2 / 'json').mkdir(parents=True)

        # Train has 'car', 'truck'
        train_json = {
            'images': [
                {
                    'bounding_box': [
                        {'classification': 'car'},
                        {'classification': 'truck'},
                    ],
                    'polygon': [],
                    'keypoint': [],
                }
            ]
        }
        (dir1 / 'json' / 'img1.json').write_text(json.dumps(train_json))

        # Val has 'car', 'bus'
        val_json = {
            'images': [
                {
                    'bounding_box': [
                        {'classification': 'car'},
                        {'classification': 'bus'},
                    ],
                    'polygon': [],
                    'keypoint': [],
                }
            ]
        }
        (dir2 / 'json' / 'img2.json').write_text(json.dumps(val_json))

        classes = FromDMToYOLOConverter.get_all_classes(
            [dir1, dir2],
            dm_version=DMVersion.V2,
        )

        assert sorted(classes) == ['bus', 'car', 'truck']

    def test_get_all_classes_ignores_none_dirs(self, temp_multi_class_dir):
        """Handles None values in directory list."""
        from synapse_sdk.plugins.datasets.converters.yolo.from_dm import DMVersion

        classes = FromDMToYOLOConverter.get_all_classes(
            [None, temp_multi_class_dir, None],
            dm_version=DMVersion.V2,
        )

        assert len(classes) > 0  # Should still find classes from valid dir


# -----------------------------------------------------------------------------
# Test polygon_to_yolo_string
# -----------------------------------------------------------------------------


class TestPolygonToYoloString:
    """Tests for FromDMToYOLOConverter.polygon_to_yolo_string method."""

    @pytest.fixture
    def converter(self, tmp_path):
        """Create converter instance."""
        conv = FromDMToYOLOConverter(root_dir=tmp_path)
        conv.class_map = {}
        return conv

    def test_polygon_normalized_correctly(self, converter):
        """Polygon points are normalized by width/height."""
        polygon = [[0, 0], [800, 0], [800, 600], [0, 600]]
        result = converter.polygon_to_yolo_string(polygon, width=800, height=600)

        # Should be: "0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0"
        parts = result.split()
        assert len(parts) == 8

        expected = [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0]
        for i, (actual, exp) in enumerate(zip(parts, expected)):
            assert abs(float(actual) - exp) < 0.001, f'Point {i} mismatch'

    def test_polygon_empty(self, converter):
        """Empty polygon returns empty string."""
        result = converter.polygon_to_yolo_string([], width=800, height=600)
        assert result == ''

    def test_polygon_single_point(self, converter):
        """Single point polygon."""
        polygon = [[400, 300]]
        result = converter.polygon_to_yolo_string(polygon, width=800, height=600)

        parts = result.split()
        assert len(parts) == 2
        assert abs(float(parts[0]) - 0.5) < 0.001
        assert abs(float(parts[1]) - 0.5) < 0.001
