import os
import json
import tempfile

import pytest
from mempalace.config import (
    MempalaceConfig,
    normalize_wing_name,
    sanitize_iso_date,
    sanitize_iso_temporal,
    sanitize_kg_value,
    sanitize_name,
)


def test_default_config():
    cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
    assert "palace" in cfg.palace_path
    assert cfg.collection_name == "mempalace_drawers"


def test_config_from_file():
    tmpdir = tempfile.mkdtemp()
    with open(os.path.join(tmpdir, "config.json"), "w") as f:
        json.dump({"palace_path": "/custom/palace"}, f)
    cfg = MempalaceConfig(config_dir=tmpdir)
    assert cfg.palace_path == "/custom/palace"


def test_embedding_device_defaults_to_auto(monkeypatch):
    monkeypatch.delenv("MEMPALACE_EMBEDDING_DEVICE", raising=False)
    cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
    assert cfg.embedding_device == "auto"


def test_embedding_device_from_config_is_normalized(tmp_path, monkeypatch):
    monkeypatch.delenv("MEMPALACE_EMBEDDING_DEVICE", raising=False)
    with open(tmp_path / "config.json", "w") as f:
        json.dump({"embedding_device": "  CUDA  "}, f)

    cfg = MempalaceConfig(config_dir=str(tmp_path))
    assert cfg.embedding_device == "cuda"


def test_embedding_device_env_overrides_config(tmp_path, monkeypatch):
    with open(tmp_path / "config.json", "w") as f:
        json.dump({"embedding_device": "cpu"}, f)
    monkeypatch.setenv("MEMPALACE_EMBEDDING_DEVICE", "  CoreML  ")

    cfg = MempalaceConfig(config_dir=str(tmp_path))
    assert cfg.embedding_device == "coreml"


def test_env_override():
    raw = "/env/palace"
    os.environ["MEMPALACE_PALACE_PATH"] = raw
    try:
        cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
        # palace_path normalizes with abspath + expanduser to match the
        # --palace CLI code path. On Unix that's a no-op for "/env/palace";
        # on Windows abspath prepends the current drive letter.
        assert cfg.palace_path == os.path.abspath(os.path.expanduser(raw))
    finally:
        del os.environ["MEMPALACE_PALACE_PATH"]


def test_env_path_expanduser():
    # Tilde must be expanded to match the --palace CLI code path. We don't
    # assert "~" is absent from the final string because Windows 8.3 short
    # paths (e.g. C:\Users\RUNNER~1\...) legitimately contain tildes — the
    # equality check is authoritative.
    raw = os.path.join("~", "mempalace-test")
    os.environ["MEMPALACE_PALACE_PATH"] = raw
    try:
        cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
        assert cfg.palace_path == os.path.abspath(os.path.expanduser(raw))
        assert cfg.palace_path.endswith("mempalace-test")
    finally:
        del os.environ["MEMPALACE_PALACE_PATH"]


def test_env_path_abspath_collapses_traversal():
    # Build a raw path with a .. segment using the platform separator so
    # the assertion is portable (Windows uses \, POSIX uses /).
    raw = os.path.join(tempfile.gettempdir(), "palace", "..", "mempalace-test")
    expected = os.path.abspath(os.path.expanduser(raw))
    os.environ["MEMPALACE_PALACE_PATH"] = raw
    try:
        cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
        # .. segments must be collapsed, not preserved literally.
        assert ".." not in cfg.palace_path
        assert cfg.palace_path == expected
    finally:
        del os.environ["MEMPALACE_PALACE_PATH"]


def test_env_path_legacy_alias_normalized():
    # Legacy MEMPAL_PALACE_PATH gets the same normalization treatment as
    # MEMPALACE_PALACE_PATH. We don't assert "~" is absent from the final
    # string because Windows 8.3 short paths (e.g. C:\Users\RUNNER~1\...)
    # legitimately contain tildes — the equality check below is authoritative.
    os.environ.pop("MEMPALACE_PALACE_PATH", None)
    raw = os.path.join("~", "legacy-alias", "..", "mempalace-test")
    os.environ["MEMPAL_PALACE_PATH"] = raw
    try:
        cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
        assert ".." not in cfg.palace_path
        assert cfg.palace_path == os.path.abspath(os.path.expanduser(raw))
    finally:
        del os.environ["MEMPAL_PALACE_PATH"]


def test_init():
    tmpdir = tempfile.mkdtemp()
    cfg = MempalaceConfig(config_dir=tmpdir)
    cfg.init()
    assert os.path.exists(os.path.join(tmpdir, "config.json"))


# --- normalize_wing_name ---


def test_normalize_wing_name_hyphen():
    assert normalize_wing_name("mempal-private") == "mempal_private"


def test_normalize_wing_name_space():
    assert normalize_wing_name("My Project") == "my_project"


def test_normalize_wing_name_already_clean():
    assert normalize_wing_name("memorymark") == "memorymark"


def test_normalize_wing_name_mixed():
    assert normalize_wing_name("My-Cool App") == "my_cool_app"


# --- sanitize_name ---


def test_sanitize_name_ascii():
    assert sanitize_name("hello") == "hello"


def test_sanitize_name_latvian():
    assert sanitize_name("Jānis") == "Jānis"


def test_sanitize_name_cjk():
    assert sanitize_name("太郎") == "太郎"


def test_sanitize_name_cyrillic():
    assert sanitize_name("Алексей") == "Алексей"


def test_sanitize_name_rejects_leading_underscore():
    with pytest.raises(ValueError):
        sanitize_name("_foo")


def test_sanitize_name_rejects_path_traversal():
    with pytest.raises(ValueError):
        sanitize_name("../etc/passwd")


def test_sanitize_name_rejects_empty():
    with pytest.raises(ValueError):
        sanitize_name("")


# --- sanitize_kg_value ---


def test_kg_value_accepts_commas():
    assert sanitize_kg_value("Alice, Bob, and Carol") == "Alice, Bob, and Carol"


def test_kg_value_accepts_colons():
    assert sanitize_kg_value("role: engineer") == "role: engineer"


def test_kg_value_accepts_parentheses():
    assert sanitize_kg_value("Python (programming)") == "Python (programming)"


def test_kg_value_accepts_slashes():
    assert sanitize_kg_value("owner/repo") == "owner/repo"


def test_kg_value_accepts_hash():
    assert sanitize_kg_value("issue #123") == "issue #123"


def test_kg_value_accepts_unicode():
    assert sanitize_kg_value("Jānis Bērziņš") == "Jānis Bērziņš"


def test_kg_value_strips_whitespace():
    assert sanitize_kg_value("  hello  ") == "hello"


def test_kg_value_rejects_empty():
    with pytest.raises(ValueError):
        sanitize_kg_value("")


def test_kg_value_rejects_whitespace_only():
    with pytest.raises(ValueError):
        sanitize_kg_value("   ")


def test_kg_value_rejects_null_bytes():
    with pytest.raises(ValueError):
        sanitize_kg_value("hello\x00world")


def test_kg_value_rejects_over_length():
    with pytest.raises(ValueError):
        sanitize_kg_value("a" * 129)


# --- sanitize_iso_date ---


def test_iso_date_rejects_year_only():
    # Partial dates re-introduce silent empty result sets via lexicographic
    # TEXT comparison in KG queries (e.g. "2026-01-01" <= "2026" is False).
    with pytest.raises(ValueError):
        sanitize_iso_date("2026")


def test_iso_date_rejects_year_month():
    with pytest.raises(ValueError):
        sanitize_iso_date("2026-03")


def test_iso_date_accepts_full_date():
    assert sanitize_iso_date("2026-03-15") == "2026-03-15"


def test_iso_date_passes_through_none():
    assert sanitize_iso_date(None) is None


def test_iso_date_passes_through_empty_string():
    assert sanitize_iso_date("") == ""


def test_iso_date_strips_whitespace():
    assert sanitize_iso_date("  2026-03-15  ") == "2026-03-15"


def test_iso_date_rejects_natural_language():
    with pytest.raises(ValueError):
        sanitize_iso_date("March 2026")


def test_iso_date_rejects_abbreviated_month():
    with pytest.raises(ValueError):
        sanitize_iso_date("Jan 2025")


def test_iso_date_rejects_us_format():
    with pytest.raises(ValueError):
        sanitize_iso_date("03/15/2026")


def test_iso_date_rejects_invalid_month():
    with pytest.raises(ValueError):
        sanitize_iso_date("2026-13")


def test_iso_date_rejects_invalid_day():
    with pytest.raises(ValueError):
        sanitize_iso_date("2026-02-32")


def test_iso_date_rejects_non_string():
    with pytest.raises(ValueError):
        sanitize_iso_date(20260315)


def test_iso_date_error_names_field():
    with pytest.raises(ValueError, match="valid_from"):
        sanitize_iso_date("yesterday", "valid_from")


def test_iso_temporal_accepts_full_date():
    assert sanitize_iso_temporal("2026-05-06") == "2026-05-06"


def test_iso_temporal_accepts_canonical_utc_datetime():
    assert sanitize_iso_temporal("2026-05-06T14:23:00Z") == "2026-05-06T14:23:00Z"


def test_iso_temporal_strips_datetime_whitespace():
    assert sanitize_iso_temporal(" 2026-05-06T14:23:00Z ") == "2026-05-06T14:23:00Z"


def test_iso_date_backward_compatible_wrapper_accepts_datetime():
    assert sanitize_iso_date("2026-05-06T14:23:00Z") == "2026-05-06T14:23:00Z"


def test_iso_temporal_rejects_datetime_without_seconds():
    with pytest.raises(ValueError):
        sanitize_iso_temporal("2026-05-06T14:23")


def test_iso_temporal_rejects_naive_datetime():
    with pytest.raises(ValueError):
        sanitize_iso_temporal("2026-05-06T14:23:00")


def test_iso_temporal_rejects_fractional_seconds():
    with pytest.raises(ValueError):
        sanitize_iso_temporal("2026-05-06T14:23:00.123Z")


def test_iso_temporal_rejects_timezone_offset():
    with pytest.raises(ValueError):
        sanitize_iso_temporal("2026-05-06T14:23:00+02:00")


def test_iso_temporal_rejects_space_separator():
    with pytest.raises(ValueError):
        sanitize_iso_temporal("2026-05-06 14:23:00")


def test_iso_temporal_rejects_invalid_datetime_hour():
    with pytest.raises(ValueError):
        sanitize_iso_temporal("2026-05-06T24:00:00Z")


def test_iso_temporal_rejects_invalid_calendar_date():
    with pytest.raises(ValueError):
        sanitize_iso_temporal("2026-02-31")


def test_iso_temporal_error_names_field():
    with pytest.raises(ValueError, match="as_of"):
        sanitize_iso_temporal("2026-05-06T14:23", "as_of")


def test_iso_temporal_normalizes_plus_zero_offset_to_z():
    assert sanitize_iso_temporal("2026-05-06T14:23:00+00:00") == "2026-05-06T14:23:00Z"
