"""Tests for curriculum learning — config, sorting, bucket creation."""

from soup_cli.config.schema import SoupConfig, TrainingConfig

# ─── Config Tests ─────────────────────────────────────────────────────────


class TestCurriculumConfig:
    """Test curriculum learning fields in TrainingConfig."""

    def test_curriculum_default_false(self):
        """curriculum should default to False."""
        tcfg = TrainingConfig()
        assert tcfg.curriculum is False

    def test_curriculum_true(self):
        tcfg = TrainingConfig(curriculum=True)
        assert tcfg.curriculum is True

    def test_curriculum_metric_default(self):
        """curriculum_metric should default to 'length'."""
        tcfg = TrainingConfig()
        assert tcfg.curriculum_metric == "length"

    def test_curriculum_metric_length(self):
        tcfg = TrainingConfig(curriculum_metric="length")
        assert tcfg.curriculum_metric == "length"

    def test_curriculum_metric_perplexity(self):
        tcfg = TrainingConfig(curriculum_metric="perplexity")
        assert tcfg.curriculum_metric == "perplexity"

    def test_curriculum_metric_loss(self):
        tcfg = TrainingConfig(curriculum_metric="loss")
        assert tcfg.curriculum_metric == "loss"

    def test_curriculum_buckets_default(self):
        """curriculum_buckets should default to 4."""
        tcfg = TrainingConfig()
        assert tcfg.curriculum_buckets == 4

    def test_curriculum_buckets_custom(self):
        tcfg = TrainingConfig(curriculum_buckets=8)
        assert tcfg.curriculum_buckets == 8

    def test_curriculum_in_full_config(self):
        cfg = SoupConfig(
            base="test-model",
            data={"train": "data.jsonl"},
            training={
                "curriculum": True,
                "curriculum_metric": "length",
                "curriculum_buckets": 4,
            },
        )
        assert cfg.training.curriculum is True
        assert cfg.training.curriculum_metric == "length"
        assert cfg.training.curriculum_buckets == 4


# ─── YAML Config Loading Tests ────────────────────────────────────────────


class TestCurriculumYamlConfig:
    """Test curriculum via YAML config loading."""

    def test_load_config_with_curriculum(self):
        from soup_cli.config.loader import load_config_from_string

        yaml_str = """
base: test-model
data:
  train: data.jsonl
training:
  curriculum: true
  curriculum_metric: length
  curriculum_buckets: 6
"""
        cfg = load_config_from_string(yaml_str)
        assert cfg.training.curriculum is True
        assert cfg.training.curriculum_metric == "length"
        assert cfg.training.curriculum_buckets == 6

    def test_load_config_without_curriculum(self):
        from soup_cli.config.loader import load_config_from_string

        yaml_str = """
base: test-model
data:
  train: data.jsonl
"""
        cfg = load_config_from_string(yaml_str)
        assert cfg.training.curriculum is False


# ─── Curriculum Sorting Tests ─────────────────────────────────────────────


class TestCurriculumSorting:
    """Test curriculum sorting by different metrics."""

    def test_sort_by_length(self):
        """Sort by length should order short → long."""
        from soup_cli.utils.curriculum import sort_by_length

        data = [
            {"text": "a" * 100},
            {"text": "a" * 10},
            {"text": "a" * 50},
        ]
        sorted_data = sort_by_length(data)
        lengths = [len(row["text"]) for row in sorted_data]
        assert lengths == [10, 50, 100]

    def test_sort_by_length_messages_format(self):
        """Sort by length should work with messages format."""
        from soup_cli.utils.curriculum import sort_by_length

        data = [
            {"messages": [{"role": "user", "content": "a" * 100}]},
            {"messages": [{"role": "user", "content": "short"}]},
            {"messages": [{"role": "user", "content": "a" * 50}]},
        ]
        sorted_data = sort_by_length(data)
        # First should be shortest
        assert len(str(sorted_data[0])) < len(str(sorted_data[-1]))

    def test_sort_by_length_empty_list(self):
        """Sort by length should handle empty list."""
        from soup_cli.utils.curriculum import sort_by_length

        assert sort_by_length([]) == []

    def test_sort_by_length_single_item(self):
        """Sort by length should handle single item."""
        from soup_cli.utils.curriculum import sort_by_length

        data = [{"text": "hello"}]
        assert sort_by_length(data) == data

    def test_sort_by_length_fallback_json(self):
        """Sort by length should fall back to JSON stringify for unknown formats."""
        from soup_cli.utils.curriculum import sort_by_length

        data = [
            {"custom": "a" * 100, "extra": "b" * 50},
            {"custom": "short"},
        ]
        sorted_data = sort_by_length(data)
        # Shorter row should come first
        assert len(str(sorted_data[0])) < len(str(sorted_data[-1]))


# ─── Curriculum Metric Fallback Tests ────────────────────────────────────


class TestCurriculumMetricFallback:
    """Test non-length metric fallback behavior."""

    def test_perplexity_metric_config(self):
        """curriculum_metric=perplexity should be a valid config."""
        cfg = SoupConfig(
            base="test-model",
            data={"train": "data.jsonl"},
            training={
                "curriculum": True,
                "curriculum_metric": "perplexity",
            },
        )
        assert cfg.training.curriculum_metric == "perplexity"

    def test_loss_metric_config(self):
        """curriculum_metric=loss should be a valid config."""
        cfg = SoupConfig(
            base="test-model",
            data={"train": "data.jsonl"},
            training={
                "curriculum": True,
                "curriculum_metric": "loss",
            },
        )
        assert cfg.training.curriculum_metric == "loss"

    def test_non_length_metric_falls_back_to_length(self):
        """Non-length metrics should fall back to length sorting in SFT trainer."""
        from io import StringIO

        from rich.console import Console

        cfg = SoupConfig(
            base="test-model",
            data={"train": "data.jsonl"},
            training={
                "curriculum": True,
                "curriculum_metric": "perplexity",
            },
        )

        output = StringIO()
        console = Console(file=output)
        tcfg = cfg.training

        # Simulate the trainer logic
        if tcfg.curriculum and tcfg.curriculum_metric != "length":
            console.print(
                f"[yellow]Curriculum metric '{tcfg.curriculum_metric}' "
                "requires pre-computed scores. Using length-based sorting.[/]"
            )

        text = output.getvalue()
        assert "perplexity" in text
        assert "length-based" in text


# ─── Bucket Creation Tests ───────────────────────────────────────────────


class TestCurriculumBuckets:
    """Test bucket creation for staged training."""

    def test_create_buckets(self):
        """create_buckets should split sorted data into N roughly equal parts."""
        from soup_cli.utils.curriculum import create_buckets

        data = list(range(100))
        buckets = create_buckets(data, num_buckets=4)
        assert len(buckets) == 4
        # All data should be present
        flat = [item for bucket in buckets for item in bucket]
        assert sorted(flat) == data

    def test_create_buckets_uneven(self):
        """Buckets should handle non-evenly-divisible data."""
        from soup_cli.utils.curriculum import create_buckets

        data = list(range(10))
        buckets = create_buckets(data, num_buckets=3)
        assert len(buckets) == 3
        flat = [item for bucket in buckets for item in bucket]
        assert sorted(flat) == data

    def test_create_buckets_single(self):
        """Single bucket should contain all data."""
        from soup_cli.utils.curriculum import create_buckets

        data = list(range(20))
        buckets = create_buckets(data, num_buckets=1)
        assert len(buckets) == 1
        assert buckets[0] == data

    def test_create_buckets_more_than_data(self):
        """More buckets than data should still work."""
        from soup_cli.utils.curriculum import create_buckets

        data = list(range(3))
        buckets = create_buckets(data, num_buckets=5)
        # Some buckets may be empty, but all data should be present
        flat = [item for bucket in buckets for item in bucket]
        assert sorted(flat) == data


# ─── Sweep Integration Tests ─────────────────────────────────────────────


class TestCurriculumSweep:
    """Test curriculum in sweep configurations."""

    def test_curriculum_in_sweep_params(self):
        from soup_cli.commands.sweep import _parse_sweep_params

        params = _parse_sweep_params(["training.curriculum=true,false"])
        assert "training.curriculum" in params

    def test_curriculum_buckets_in_sweep_params(self):
        from soup_cli.commands.sweep import _parse_sweep_params

        params = _parse_sweep_params(["training.curriculum_buckets=2,4,8"])
        assert "training.curriculum_buckets" in params
        assert params["training.curriculum_buckets"] == [2, 4, 8]
