import re
from pathlib import Path

import pytest

import vcti.config
from vcti.config.decode import (
    decode_bool,
    decode_float,
    decode_int,
    decode_json,
    decode_path,
    decode_str,
    make_bool_decoder,
    read_env,
)
from vcti.config.errors import DecodeError


class TestDecodeBool:
    """Tests for decode_bool."""

    @pytest.mark.parametrize("value", ["1", "true", "yes", "on", "enabled"])
    def test_true_values(self, value):
        assert decode_bool(value) is True

    @pytest.mark.parametrize("value", ["0", "false", "no", "off", "disabled"])
    def test_false_values(self, value):
        assert decode_bool(value) is False

    @pytest.mark.parametrize("value", ["TRUE", "True", "YES", "On", "ENABLED"])
    def test_case_insensitive(self, value):
        assert decode_bool(value) is True

    def test_invalid_raises_decode_error(self):
        with pytest.raises(DecodeError, match="Cannot decode 'maybe' as bool"):
            decode_bool("maybe")

    def test_error_has_raw_value(self):
        with pytest.raises(DecodeError) as exc_info:
            decode_bool("nope")
        assert exc_info.value.raw_value == "nope"


class TestDecodeInt:
    """Tests for decode_int."""

    @pytest.mark.parametrize("value,expected", [("42", 42), ("-1", -1), ("0", 0)])
    def test_valid(self, value, expected):
        assert decode_int(value) == expected

    def test_invalid_raises_decode_error(self):
        with pytest.raises(DecodeError, match="Cannot decode 'abc' as int"):
            decode_int("abc")

    def test_float_string_raises(self):
        with pytest.raises(DecodeError):
            decode_int("3.14")

    def test_exception_chaining(self):
        """DecodeError should chain the original ValueError."""
        with pytest.raises(DecodeError) as exc_info:
            decode_int("abc")
        assert exc_info.value.__cause__ is not None
        assert isinstance(exc_info.value.__cause__, ValueError)


class TestDecodeFloat:
    """Tests for decode_float."""

    @pytest.mark.parametrize(
        "value,expected",
        [("3.14", 3.14), ("-1.5", -1.5), ("0.0", 0.0), ("42", 42.0)],
    )
    def test_valid(self, value, expected):
        assert decode_float(value) == expected

    def test_invalid_raises_decode_error(self):
        with pytest.raises(DecodeError, match="Cannot decode 'xyz' as float"):
            decode_float("xyz")

    def test_exception_chaining(self):
        """DecodeError should chain the original ValueError."""
        with pytest.raises(DecodeError) as exc_info:
            decode_float("xyz")
        assert isinstance(exc_info.value.__cause__, ValueError)


class TestDecodeStr:
    """Tests for decode_str."""

    def test_strips_whitespace(self):
        assert decode_str("  hello  ") == "hello"

    def test_passthrough(self):
        assert decode_str("value") == "value"


class TestDecodePath:
    """Tests for decode_path."""

    def test_returns_path(self):
        result = decode_path("/tmp/file")
        assert result == Path("/tmp/file")
        assert isinstance(result, Path)

    def test_strips_whitespace(self):
        assert decode_path("  /tmp/file  ") == Path("/tmp/file")


class TestDecodeJson:
    """Tests for decode_json."""

    def test_object(self):
        assert decode_json('{"key": "value"}') == {"key": "value"}

    def test_array(self):
        assert decode_json("[1, 2, 3]") == [1, 2, 3]

    def test_scalar(self):
        assert decode_json("42") == 42

    def test_invalid_raises_decode_error(self):
        with pytest.raises(DecodeError, match="Cannot decode"):
            decode_json("{invalid}")

    def test_exception_chaining(self):
        """DecodeError should chain the original JSONDecodeError."""
        with pytest.raises(DecodeError) as exc_info:
            decode_json("{invalid}")
        assert exc_info.value.__cause__ is not None


class TestMakeBoolDecoder:
    """Tests for make_bool_decoder factory."""

    def test_custom_values(self):
        decoder = make_bool_decoder({"si", "oui"}, {"non", "nein"})
        assert decoder("si") is True
        assert decoder("oui") is True
        assert decoder("non") is False
        assert decoder("nein") is False

    def test_custom_invalid_raises(self):
        decoder = make_bool_decoder({"y"}, {"n"})
        with pytest.raises(DecodeError):
            decoder("maybe")

    def test_case_insensitive(self):
        decoder = make_bool_decoder({"yes"}, {"no"})
        assert decoder("YES") is True
        assert decoder("NO") is False


class TestReadEnv:
    """Tests for read_env."""

    def test_reads_from_env(self, monkeypatch):
        monkeypatch.setenv("TEST_VAR", "hello")
        assert read_env("TEST_VAR") == "hello"

    def test_strips_whitespace(self, monkeypatch):
        monkeypatch.setenv("TEST_VAR", "  value  ")
        assert read_env("TEST_VAR") == "value"

    def test_missing_returns_none(self, monkeypatch):
        monkeypatch.delenv("TEST_VAR", raising=False)
        assert read_env("TEST_VAR") is None

    def test_blank_returns_none(self, monkeypatch):
        monkeypatch.setenv("TEST_VAR", "   ")
        assert read_env("TEST_VAR") is None

    def test_empty_returns_none(self, monkeypatch):
        monkeypatch.setenv("TEST_VAR", "")
        assert read_env("TEST_VAR") is None

    def test_custom_env_dict(self):
        env = {"MY_KEY": "  val  "}
        assert read_env("MY_KEY", env) == "val"

    def test_custom_env_missing_key(self):
        assert read_env("NOPE", {}) is None


class TestReadEnvWithDecoder:
    """Integration: read_env + decoder compose for standalone concept 2."""

    def test_read_and_decode_int(self, monkeypatch):
        monkeypatch.setenv("PORT", "8080")
        raw = read_env("PORT")
        assert raw is not None
        assert decode_int(raw) == 8080

    def test_read_missing_skips_decode(self, monkeypatch):
        monkeypatch.delenv("PORT", raising=False)
        raw = read_env("PORT")
        assert raw is None
        # Caller uses default instead of calling decoder
        port = decode_int(raw) if raw else 8080
        assert port == 8080


class TestVersion:
    def test_version_exists(self):
        assert hasattr(vcti.config, "__version__")

    def test_version_is_valid_semver(self):
        assert re.match(r"^\d+\.\d+\.\d+", vcti.config.__version__)
