"""設定スキーマのテスト（SPEC_01 基本方針 3・testing ルール必須対象 5）."""

from pathlib import Path

import pytest
from pydantic import ValidationError

from tias_judge_model.schema import DATA_ROOT_ENV_VAR, ExperimentConfig, load_config

REPO_ROOT = Path(__file__).resolve().parents[1]

VALID_YAML = """\
data:
  root: some/data
  manifest: manifest.csv
split:
  seed: 7
  val_ratio: 0.1
  test_ratio: 0.2
  n_folds: 5
"""


def _write(tmp_path: Path, text: str) -> Path:
    path = tmp_path / "config.yaml"
    path.write_text(text, encoding="utf-8")
    return path


def test_load_valid_config(tmp_path: Path) -> None:
    config = load_config(_write(tmp_path, VALID_YAML), environ={})
    assert isinstance(config, ExperimentConfig)
    assert config.data.root == Path("some/data")
    assert config.data.manifest_path == Path("some/data") / "manifest.csv"
    assert config.data.images_dir == Path("some/data") / "images"
    assert config.split.seed == 7
    assert config.split.val_ratio == pytest.approx(0.1)
    assert config.split.test_ratio == pytest.approx(0.2)
    assert config.split.n_folds == 5


def test_split_section_is_optional(tmp_path: Path) -> None:
    config = load_config(_write(tmp_path, "data:\n  root: d\n"), environ={})
    assert config.split.n_folds == 5  # SPEC_02: 結論を出すときは 5-fold
    assert config.split.val_ratio + config.split.test_ratio < 1.0


@pytest.mark.parametrize("name", ["synthetic.yaml", "newton.yaml"])
def test_repository_configs_are_valid(name: str) -> None:
    config = load_config(REPO_ROOT / "configs" / name, environ={})
    assert config.split.n_folds == 5


def test_env_var_overrides_yaml_data_root(tmp_path: Path) -> None:
    env_root = tmp_path / "from_env"
    config = load_config(_write(tmp_path, VALID_YAML), environ={DATA_ROOT_ENV_VAR: str(env_root)})
    assert DATA_ROOT_ENV_VAR == "TIAS_DATA_ROOT"
    assert config.data.root == env_root
    assert config.data.manifest_path == env_root / "manifest.csv"


def test_env_var_supplies_root_missing_from_yaml(tmp_path: Path) -> None:
    config = load_config(
        _write(tmp_path, "split:\n  seed: 1\n"), environ={DATA_ROOT_ENV_VAR: "env/root"}
    )
    assert config.data.root == Path("env/root")


def test_yaml_root_used_when_env_var_absent(tmp_path: Path) -> None:
    config = load_config(_write(tmp_path, VALID_YAML), environ={"OTHER": "x"})
    assert config.data.root == Path("some/data")


def test_os_environ_is_used_by_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv(DATA_ROOT_ENV_VAR, "from/os/environ")
    config = load_config(_write(tmp_path, VALID_YAML))
    assert config.data.root == Path("from/os/environ")


@pytest.mark.parametrize(
    "split_yaml",
    [
        "  val_ratio: 0.5\n  test_ratio: 0.5\n",  # 合計 1
        "  val_ratio: 0.6\n  test_ratio: 0.5\n",  # 合計 1 超
        "  val_ratio: -0.1\n",
        "  test_ratio: 1.0\n",
        "  n_folds: 1\n",
        "  n_folds: 0\n",
        "  seed: not_a_number\n",
        "  n_foldz: 5\n",  # 未知のキー（綴り間違い）
    ],
)
def test_invalid_split_is_rejected(tmp_path: Path, split_yaml: str) -> None:
    text = "data:\n  root: d\nsplit:\n" + split_yaml
    with pytest.raises(ValidationError):
        load_config(_write(tmp_path, text), environ={})


@pytest.mark.parametrize(
    "text",
    [
        "split:\n  seed: 1\n",  # data が無い
        "data:\n  manifest: m.csv\n",  # data.root が無い
        "data:\n  root: d\n  rooot: e\n",  # data の未知のキー
        "data:\n  root: d\nmodel: convnext\n",  # トップレベルの未知のキー
    ],
)
def test_missing_required_or_unknown_key_is_rejected(tmp_path: Path, text: str) -> None:
    with pytest.raises(ValidationError):
        load_config(_write(tmp_path, text), environ={})


def test_empty_yaml_is_rejected(tmp_path: Path) -> None:
    with pytest.raises(ValidationError):
        load_config(_write(tmp_path, ""), environ={})


def test_non_mapping_yaml_is_rejected(tmp_path: Path) -> None:
    with pytest.raises(ValueError):
        load_config(_write(tmp_path, "- a\n- b\n"), environ={})


# --- data.severities（SPEC_01「設定ファイル」・SPEC_02「程度（ややあり）の扱い」） ---


def test_severities_default_is_all_three(tmp_path: Path) -> None:
    config = load_config(_write(tmp_path, "data:\n  root: d\n"), environ={})
    assert set(config.data.severities) == {"strong", "mild", "normal"}
    assert len(config.data.severities) == 3


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("[strong, normal]", {"strong", "normal"}),  # ややあり除外
        ("[strong, mild, normal]", {"strong", "mild", "normal"}),  # ややあり込み
        ("[normal, strong]", {"strong", "normal"}),  # 記載順は問わない
        ("[mild]", {"mild"}),
    ],
)
def test_valid_severities_are_accepted(tmp_path: Path, value: str, expected: set[str]) -> None:
    config = load_config(_write(tmp_path, f"data:\n  root: d\n  severities: {value}\n"), environ={})
    assert set(config.data.severities) == expected
    assert len(config.data.severities) == len(expected)


@pytest.mark.parametrize(
    "value",
    [
        "[]",  # 空
        "[strong, strong]",  # 重複
        "[strong, normal, strong]",  # 重複
        "[weak]",  # 未知の値
        "[strong, Mild]",  # 大文字違いも未知の値
        "[strong, ややあり]",
    ],
)
def test_invalid_severities_are_rejected(tmp_path: Path, value: str) -> None:
    text = f"data:\n  root: d\n  severities: {value}\n"
    with pytest.raises(ValidationError):
        load_config(_write(tmp_path, text), environ={})
