"""
CLI integration tests for SLTK.

Tests all CLI commands, argument parsing, and error handling.
"""

import json
from pathlib import Path
from unittest.mock import MagicMock, patch

import numpy as np
import pytest
from click.testing import CliRunner

from sltk.cli.main import main


@pytest.fixture
def runner():
    """Create a CLI runner for testing."""
    return CliRunner()


class TestMainGroup:
    """Tests for main CLI group."""

    def test_help(self, runner):
        """Test --help flag."""
        result = runner.invoke(main, ["--help"])
        assert result.exit_code == 0
        assert "Sign Language Toolkit" in result.output

    def test_version(self, runner):
        """Test --version flag."""
        result = runner.invoke(main, ["--version"])
        assert result.exit_code == 0
        # Should show version number


class TestConvertCommand:
    """Tests for the convert command."""

    def test_convert_help(self, runner):
        """Test convert --help."""
        result = runner.invoke(main, ["convert", "--help"])
        assert result.exit_code == 0
        assert "--from" in result.output
        assert "--to" in result.output

    def test_convert_missing_from_format(self, runner, tmp_path):
        """Test convert fails without --from."""
        input_file = tmp_path / "input.h5"
        input_file.touch()
        output_file = tmp_path / "output.h5"

        result = runner.invoke(
            main,
            ["convert", str(input_file), str(output_file), "--to", "mediapipe"],
        )
        assert result.exit_code != 0
        assert "Missing option" in result.output or "required" in result.output.lower()

    def test_convert_missing_to_format(self, runner, tmp_path):
        """Test convert fails without --to."""
        input_file = tmp_path / "input.h5"
        input_file.touch()
        output_file = tmp_path / "output.h5"

        result = runner.invoke(
            main,
            ["convert", str(input_file), str(output_file), "--from", "wilor"],
        )
        assert result.exit_code != 0
        assert "Missing option" in result.output or "required" in result.output.lower()

    def test_convert_nonexistent_input(self, runner, tmp_path):
        """Test convert fails with nonexistent input file."""
        result = runner.invoke(
            main,
            [
                "convert",
                "/nonexistent/path/file.h5",
                str(tmp_path / "output.h5"),
                "--from",
                "wilor",
                "--to",
                "mediapipe",
            ],
        )
        assert result.exit_code != 0

    @patch("sltk.data.core.PoseSequence")
    def test_convert_success(self, mock_pose_seq, runner, tmp_path):
        """Test successful convert command with mocked PoseSequence."""
        input_file = tmp_path / "input.h5"
        input_file.touch()
        output_file = tmp_path / "output.h5"

        # Mock the PoseSequence class
        mock_instance = MagicMock()
        mock_instance.num_frames = 100
        mock_pose_seq.load.return_value = mock_instance
        mock_instance.to_format.return_value = mock_instance

        runner.invoke(
            main,
            [
                "convert",
                str(input_file),
                str(output_file),
                "--from",
                "wilor",
                "--to",
                "mediapipe",
                "--fps",
                "30.0",
            ],
        )

        # Even if it fails due to file format, the CLI structure is tested


class TestEvaluateCommand:
    """Tests for the evaluate command."""

    def test_evaluate_help(self, runner):
        """Test evaluate --help."""
        result = runner.invoke(main, ["evaluate", "--help"])
        assert result.exit_code == 0
        assert "--task" in result.output
        assert "translation" in result.output

    def test_evaluate_missing_task(self, runner, temp_predictions_file, temp_references_file):
        """Test evaluate fails without --task."""
        result = runner.invoke(
            main,
            [
                "evaluate",
                str(temp_predictions_file),
                str(temp_references_file),
            ],
        )
        assert result.exit_code != 0
        assert "Missing option" in result.output or "required" in result.output.lower()

    def test_evaluate_nonexistent_predictions(self, runner, tmp_path):
        """Test evaluate fails with nonexistent predictions file."""
        refs = tmp_path / "refs.txt"
        refs.write_text("hello\n")

        result = runner.invoke(
            main,
            [
                "evaluate",
                "/nonexistent/preds.txt",
                str(refs),
                "--task",
                "translation",
            ],
        )
        assert result.exit_code != 0

    def test_evaluate_nonexistent_references(self, runner, tmp_path):
        """Test evaluate fails with nonexistent references file."""
        preds = tmp_path / "preds.txt"
        preds.write_text("hello\n")

        result = runner.invoke(
            main,
            [
                "evaluate",
                str(preds),
                "/nonexistent/refs.txt",
                "--task",
                "translation",
            ],
        )
        assert result.exit_code != 0

    def test_evaluate_translation_length_mismatch(self, runner, tmp_path):
        """Test evaluate fails with mismatched line counts."""
        preds = tmp_path / "preds.txt"
        refs = tmp_path / "refs.txt"
        preds.write_text("hello\nworld\n")
        refs.write_text("hello\n")

        result = runner.invoke(
            main,
            [
                "evaluate",
                str(preds),
                str(refs),
                "--task",
                "translation",
            ],
        )
        assert result.exit_code != 0
        assert "mismatch" in result.output.lower() or "Error" in result.output

    def test_evaluate_translation_empty_files(self, runner, tmp_path):
        """Test evaluate fails with empty files."""
        preds = tmp_path / "preds.txt"
        refs = tmp_path / "refs.txt"
        preds.write_text("")
        refs.write_text("")

        result = runner.invoke(
            main,
            [
                "evaluate",
                str(preds),
                str(refs),
                "--task",
                "translation",
            ],
        )
        assert result.exit_code != 0

    @patch("sltk.metrics.translation.translation_metrics")
    def test_evaluate_translation_success(
        self, mock_metrics, runner, temp_predictions_file, temp_references_file
    ):
        """Test successful translation evaluation."""
        mock_metrics.return_value = {
            "bleu1": 50.0,
            "bleu4": 30.0,
            "rouge_l": 0.6,
        }

        result = runner.invoke(
            main,
            [
                "evaluate",
                str(temp_predictions_file),
                str(temp_references_file),
                "--task",
                "translation",
            ],
        )

        assert result.exit_code == 0
        assert "Results" in result.output

    @patch("sltk.metrics.translation.translation_metrics")
    def test_evaluate_with_output_file(
        self, mock_metrics, runner, temp_predictions_file, temp_references_file, tmp_path
    ):
        """Test evaluate with --output flag."""
        mock_metrics.return_value = {
            "bleu1": 50.0,
            "bleu4": 30.0,
        }

        output_file = tmp_path / "results.json"

        result = runner.invoke(
            main,
            [
                "evaluate",
                str(temp_predictions_file),
                str(temp_references_file),
                "--task",
                "translation",
                "--output",
                str(output_file),
            ],
        )

        assert result.exit_code == 0
        assert output_file.exists()

        with open(output_file) as f:
            data = json.load(f)
        assert "bleu1" in data


class TestToElanCommand:
    """Tests for the to-elan command."""

    def test_to_elan_help(self, runner):
        """Test to-elan --help."""
        result = runner.invoke(main, ["to-elan", "--help"])
        assert result.exit_code == 0
        assert "--output" in result.output

    def test_to_elan_missing_output(self, runner, temp_json_segments):
        """Test to-elan fails without --output."""
        result = runner.invoke(main, ["to-elan", str(temp_json_segments)])
        assert result.exit_code != 0
        assert "Missing option" in result.output or "required" in result.output.lower()

    def test_to_elan_nonexistent_input(self, runner, tmp_path):
        """Test to-elan fails with nonexistent input."""
        result = runner.invoke(
            main,
            [
                "to-elan",
                "/nonexistent/segments.json",
                "--output",
                str(tmp_path / "output.eaf"),
            ],
        )
        assert result.exit_code != 0

    def test_to_elan_invalid_json(self, runner, tmp_path):
        """Test to-elan fails with invalid JSON."""
        bad_json = tmp_path / "bad.json"
        bad_json.write_text("not valid json {{{")

        result = runner.invoke(
            main,
            [
                "to-elan",
                str(bad_json),
                "--output",
                str(tmp_path / "output.eaf"),
            ],
        )
        assert result.exit_code != 0
        assert "Invalid JSON" in result.output or "Error" in result.output

    def test_to_elan_invalid_schema(self, runner, tmp_path):
        """Test to-elan fails with invalid schema (not a list)."""
        bad_schema = tmp_path / "bad_schema.json"
        bad_schema.write_text('{"not": "a list"}')

        result = runner.invoke(
            main,
            [
                "to-elan",
                str(bad_schema),
                "--output",
                str(tmp_path / "output.eaf"),
            ],
        )
        assert result.exit_code != 0
        assert "Invalid" in result.output or "Error" in result.output

    def test_to_elan_empty_segments(self, runner, tmp_path):
        """Test to-elan fails with empty segments list."""
        empty = tmp_path / "empty.json"
        empty.write_text("[]")

        result = runner.invoke(
            main,
            [
                "to-elan",
                str(empty),
                "--output",
                str(tmp_path / "output.eaf"),
            ],
        )
        assert result.exit_code != 0
        assert "no segments" in result.output.lower() or "Error" in result.output

    def test_to_elan_missing_start_field(self, runner, tmp_path):
        """Test to-elan fails when segment missing 'start'."""
        bad_seg = tmp_path / "bad_seg.json"
        bad_seg.write_text('[{"end": 1.0, "label": "TEST"}]')

        result = runner.invoke(
            main,
            [
                "to-elan",
                str(bad_seg),
                "--output",
                str(tmp_path / "output.eaf"),
            ],
        )
        assert result.exit_code != 0
        assert "start" in result.output.lower() or "Error" in result.output

    def test_to_elan_success(self, runner, temp_json_segments, tmp_path):
        """Test successful to-elan conversion."""
        output_file = tmp_path / "output.eaf"

        result = runner.invoke(
            main,
            [
                "to-elan",
                str(temp_json_segments),
                "--output",
                str(output_file),
            ],
        )

        assert result.exit_code == 0
        assert output_file.exists()
        assert "Exported" in result.output

    def test_to_elan_with_tier(self, runner, temp_json_segments, tmp_path):
        """Test to-elan with custom tier name."""
        output_file = tmp_path / "output.eaf"

        result = runner.invoke(
            main,
            [
                "to-elan",
                str(temp_json_segments),
                "--output",
                str(output_file),
                "--tier",
                "CustomTier",
            ],
        )

        assert result.exit_code == 0
        assert output_file.exists()


class TestFromElanCommand:
    """Tests for the from-elan command."""

    def test_from_elan_help(self, runner):
        """Test from-elan --help."""
        result = runner.invoke(main, ["from-elan", "--help"])
        assert result.exit_code == 0
        assert "--output" in result.output
        assert "--tier" in result.output

    def test_from_elan_nonexistent_input(self, runner):
        """Test from-elan fails with nonexistent input."""
        result = runner.invoke(main, ["from-elan", "/nonexistent/file.eaf"])
        assert result.exit_code != 0

    def test_from_elan_success(self, runner, temp_elan_file):
        """Test successful from-elan import."""
        result = runner.invoke(main, ["from-elan", str(temp_elan_file)])

        assert result.exit_code == 0
        assert "Loaded" in result.output
        assert "segments" in result.output

    def test_from_elan_with_output(self, runner, temp_elan_file, tmp_path):
        """Test from-elan with JSON output."""
        output_file = tmp_path / "segments.json"

        result = runner.invoke(
            main,
            [
                "from-elan",
                str(temp_elan_file),
                "--output",
                str(output_file),
            ],
        )

        assert result.exit_code == 0
        assert output_file.exists()

        with open(output_file) as f:
            data = json.load(f)
        assert isinstance(data, list)
        assert len(data) > 0

    def test_from_elan_warns_non_eaf_extension(self, runner, tmp_path):
        """Test from-elan warns about non-.eaf extension."""
        # Create a file with wrong extension but valid content
        from sltk.data.core import Segment, SegmentList
        from sltk.io.elan import write_eaf

        segments = SegmentList([Segment(0.0, 1.0, "TEST")])
        eaf_content = tmp_path / "test.xml"  # Wrong extension
        write_eaf(segments, tmp_path / "temp.eaf")

        # Copy content to .xml file
        import shutil

        shutil.copy(tmp_path / "temp.eaf", eaf_content)

        result = runner.invoke(main, ["from-elan", str(eaf_content)])

        assert result.exit_code == 0
        assert "Warning" in result.output or "Loaded" in result.output


class TestInfoCommand:
    """Tests for the info command."""

    def test_info_help(self, runner):
        """Test info --help."""
        result = runner.invoke(main, ["info", "--help"])
        assert result.exit_code == 0

    def test_info_nonexistent_file(self, runner):
        """Test info fails with nonexistent file."""
        result = runner.invoke(main, ["info", "/nonexistent/file.h5"])
        assert result.exit_code != 0

    def test_info_unsupported_format(self, runner, tmp_path):
        """Test info fails with unsupported file type."""
        txt_file = tmp_path / "test.txt"
        txt_file.write_text("hello")

        result = runner.invoke(main, ["info", str(txt_file)])
        assert result.exit_code != 0
        assert "Unsupported" in result.output or "Error" in result.output

    def test_info_h5_file(self, runner, temp_h5_file):
        """Test info for H5 file."""
        result = runner.invoke(main, ["info", str(temp_h5_file)])

        assert result.exit_code == 0
        assert "H5 File" in result.output or "Frames" in result.output

    def test_info_elan_file(self, runner, temp_elan_file):
        """Test info for ELAN file."""
        result = runner.invoke(main, ["info", str(temp_elan_file)])

        assert result.exit_code == 0
        assert "ELAN" in result.output
        assert "segments" in result.output.lower()


class TestFormatsCommand:
    """Tests for the formats command."""

    def test_formats_help(self, runner):
        """Test formats --help."""
        result = runner.invoke(main, ["formats", "--help"])
        assert result.exit_code == 0

    def test_formats_lists_formats(self, runner):
        """Test formats shows available formats."""
        result = runner.invoke(main, ["formats"])

        assert result.exit_code == 0
        assert "Supported" in result.output


class TestConvertAnnotationsCommand:
    """Tests for the convert-annotations command."""

    def test_convert_annotations_help(self, runner):
        """Test convert-annotations --help."""
        result = runner.invoke(main, ["convert-annotations", "--help"])
        assert result.exit_code == 0
        assert "--source" in result.output
        assert "--output" in result.output

    def test_convert_annotations_missing_source(self, runner, tmp_path):
        """Test convert-annotations fails without --source."""
        result = runner.invoke(
            main,
            [
                "convert-annotations",
                "test_dataset",
                "--output",
                str(tmp_path / "output"),
            ],
        )
        assert result.exit_code != 0

    def test_convert_annotations_nonexistent_source(self, runner, tmp_path):
        """Test convert-annotations fails with nonexistent source."""
        result = runner.invoke(
            main,
            [
                "convert-annotations",
                "test_dataset",
                "--source",
                "/nonexistent/dir",
                "--output",
                str(tmp_path / "output"),
            ],
        )
        assert result.exit_code != 0

    def test_convert_annotations_source_not_directory(self, runner, tmp_path):
        """Test convert-annotations fails when source is not a directory."""
        source_file = tmp_path / "source.txt"
        source_file.write_text("not a directory")

        result = runner.invoke(
            main,
            [
                "convert-annotations",
                "test_dataset",
                "--source",
                str(source_file),
                "--output",
                str(tmp_path / "output"),
            ],
        )
        # Should fail because it's a file, not a directory
        assert result.exit_code != 0


class TestValidateAnnotationsCommand:
    """Tests for the validate-annotations command."""

    def test_validate_annotations_help(self, runner):
        """Test validate-annotations --help."""
        result = runner.invoke(main, ["validate-annotations", "--help"])
        assert result.exit_code == 0
        assert "--strict" in result.output

    def test_validate_annotations_nonexistent_path(self, runner):
        """Test validate-annotations fails with nonexistent path."""
        result = runner.invoke(
            main,
            ["validate-annotations", "/nonexistent/path"],
        )
        assert result.exit_code != 0

    def test_validate_annotations_valid_file(self, runner, temp_unified_annotation_file):
        """Test validate-annotations with valid annotation file."""
        result = runner.invoke(
            main,
            ["validate-annotations", str(temp_unified_annotation_file)],
        )

        assert result.exit_code == 0
        assert "Valid" in result.output

    def test_validate_annotations_invalid_file(self, runner, tmp_path):
        """Test validate-annotations with invalid annotation file."""
        invalid_file = tmp_path / "invalid.json"
        invalid_file.write_text('{"not": "valid schema"}')

        result = runner.invoke(
            main,
            ["validate-annotations", str(invalid_file)],
        )

        # Should report errors but complete
        assert "Invalid" in result.output or "Error" in result.output or "Missing" in result.output

    def test_validate_annotations_directory(self, runner, tmp_path, sample_unified_annotation):
        """Test validate-annotations on a directory."""
        # Create multiple annotation files
        for i in range(3):
            annotation = sample_unified_annotation.copy()
            annotation["sample_id"] = f"sample_{i}"
            path = tmp_path / f"sample_{i}.json"
            with open(path, "w") as f:
                json.dump(annotation, f)

        result = runner.invoke(
            main,
            ["validate-annotations", str(tmp_path)],
        )

        assert result.exit_code == 0
        assert "Validating 3 files" in result.output

    def test_validate_annotations_empty_directory(self, runner, tmp_path):
        """Test validate-annotations on empty directory."""
        empty_dir = tmp_path / "empty"
        empty_dir.mkdir()

        result = runner.invoke(
            main,
            ["validate-annotations", str(empty_dir)],
        )

        assert result.exit_code != 0
        assert "No JSON" in result.output or "Error" in result.output


class TestGenerateManifestCommand:
    """Tests for the generate-manifest command."""

    def test_generate_manifest_help(self, runner):
        """Test generate-manifest --help."""
        result = runner.invoke(main, ["generate-manifest", "--help"])
        assert result.exit_code == 0
        assert "--dataset" in result.output

    def test_generate_manifest_missing_dataset(self, runner, tmp_path):
        """Test generate-manifest fails without --dataset."""
        result = runner.invoke(
            main,
            ["generate-manifest", str(tmp_path)],
        )
        assert result.exit_code != 0

    def test_generate_manifest_nonexistent_dir(self, runner):
        """Test generate-manifest fails with nonexistent directory."""
        result = runner.invoke(
            main,
            ["generate-manifest", "/nonexistent/dir", "--dataset", "test"],
        )
        assert result.exit_code != 0

    def test_generate_manifest_empty_dir(self, runner, tmp_path):
        """Test generate-manifest fails on empty directory."""
        empty_dir = tmp_path / "empty"
        empty_dir.mkdir()

        result = runner.invoke(
            main,
            ["generate-manifest", str(empty_dir), "--dataset", "test"],
        )
        assert result.exit_code != 0
        assert "No annotation" in result.output or "Error" in result.output

    def test_generate_manifest_success(self, runner, tmp_path, sample_unified_annotation):
        """Test successful manifest generation."""
        # Create annotation files
        for i in range(3):
            annotation = sample_unified_annotation.copy()
            annotation["sample_id"] = f"sample_{i}"
            path = tmp_path / f"sample_{i}.json"
            with open(path, "w") as f:
                json.dump(annotation, f)

        result = runner.invoke(
            main,
            ["generate-manifest", str(tmp_path), "--dataset", "test_dataset"],
        )

        assert result.exit_code == 0
        assert "Manifest generated" in result.output
        assert (tmp_path / "manifest.json").exists()


class TestAnnotationFormatsCommand:
    """Tests for the annotation-formats command."""

    def test_annotation_formats_help(self, runner):
        """Test annotation-formats --help."""
        result = runner.invoke(main, ["annotation-formats", "--help"])
        assert result.exit_code == 0

    def test_annotation_formats_lists_formats(self, runner):
        """Test annotation-formats shows available formats."""
        result = runner.invoke(main, ["annotation-formats"])

        assert result.exit_code == 0
        assert "Supported annotation formats" in result.output
        assert "elan" in result.output.lower()


class TestAnnotationInfoCommand:
    """Tests for the annotation-info command."""

    def test_annotation_info_help(self, runner):
        """Test annotation-info --help."""
        result = runner.invoke(main, ["annotation-info", "--help"])
        assert result.exit_code == 0

    def test_annotation_info_nonexistent_path(self, runner):
        """Test annotation-info fails with nonexistent path."""
        result = runner.invoke(
            main,
            ["annotation-info", "/nonexistent/path"],
        )
        assert result.exit_code != 0

    def test_annotation_info_single_file(self, runner, temp_unified_annotation_file):
        """Test annotation-info for single file."""
        result = runner.invoke(
            main,
            ["annotation-info", str(temp_unified_annotation_file)],
        )

        assert result.exit_code == 0
        assert "Sample" in result.output
        assert "test_sample_001" in result.output

    def test_annotation_info_manifest(self, runner, tmp_path, sample_unified_annotation):
        """Test annotation-info for manifest.json."""
        from sltk.io.annotations import generate_manifest

        # Create annotation files
        for i in range(3):
            annotation = sample_unified_annotation.copy()
            annotation["sample_id"] = f"sample_{i}"
            path = tmp_path / f"sample_{i}.json"
            with open(path, "w") as f:
                json.dump(annotation, f)

        # Generate manifest
        generate_manifest(tmp_path, "test_dataset")

        manifest_path = tmp_path / "manifest.json"
        result = runner.invoke(
            main,
            ["annotation-info", str(manifest_path)],
        )

        assert result.exit_code == 0
        assert "Manifest" in result.output or "Dataset" in result.output

    def test_annotation_info_directory(self, runner, tmp_path, sample_unified_annotation):
        """Test annotation-info for directory."""
        # Create annotation files
        for i in range(3):
            annotation = sample_unified_annotation.copy()
            annotation["sample_id"] = f"sample_{i}"
            path = tmp_path / f"sample_{i}.json"
            with open(path, "w") as f:
                json.dump(annotation, f)

        result = runner.invoke(
            main,
            ["annotation-info", str(tmp_path)],
        )

        assert result.exit_code == 0
        assert "Directory" in result.output or "JSON files" in result.output
