from pathlib import Path

import pytest

from vcti.config.decode import decode_int
from vcti.config.errors import DecodeError
from vcti.config.field import _MISSING_TYPE, MISSING, Field


class TestMissingSentinel:
    """Tests for the MISSING sentinel."""

    def test_is_singleton(self):
        assert _MISSING_TYPE() is MISSING

    def test_repr(self):
        assert repr(MISSING) == "MISSING"

    def test_is_falsy(self):
        assert not MISSING


class TestFieldConstructors:
    """Tests for Field convenience constructors."""

    def test_bool_field(self):
        f = Field.bool(default=False)
        assert f.decoder.__name__ == "decode_bool"
        assert f.default is False

    def test_int_field(self):
        f = Field.int(default=42)
        assert f.decoder.__name__ == "decode_int"
        assert f.default == 42

    def test_float_field(self):
        f = Field.float(default=3.14)
        assert f.decoder.__name__ == "decode_float"
        assert f.default == 3.14

    def test_str_field(self):
        f = Field.str(default="hello")
        assert f.decoder.__name__ == "decode_str"
        assert f.default == "hello"

    def test_path_field(self):
        f = Field.path(default=Path("/tmp"))
        assert f.decoder.__name__ == "decode_path"
        assert f.default == Path("/tmp")

    def test_json_field(self):
        f = Field.json(default={"key": "val"})
        assert f.decoder.__name__ == "decode_json"
        assert f.default == {"key": "val"}

    def test_no_default(self):
        f = Field.str()
        assert isinstance(f.default, _MISSING_TYPE)

    def test_none_default_is_distinct_from_missing(self):
        """default=None is a valid default, not the same as MISSING."""
        f = Field.json(default=None)
        assert f.default is None
        assert not isinstance(f.default, _MISSING_TYPE)


class TestFieldAttributes:
    """Tests for Field attributes."""

    def test_validator(self):
        f = Field.int(default=8080, validator=lambda p: 1 <= p <= 65535)
        assert f.validator is not None
        assert f.validator(80) is True
        assert f.validator(0) is False

    def test_env_name_override(self):
        f = Field.str(env_name="CUSTOM_VAR")
        assert f.env_name == "CUSTOM_VAR"

    def test_description(self):
        f = Field.int(description="The port number")
        assert f.description == "The port number"

    def test_custom_decoder(self):
        f = Field(decode_int, default=0)
        assert f.decoder is decode_int
        assert f.default == 0

    def test_sensitive_default_false(self):
        f = Field.str(default="x")
        assert f.sensitive is False

    def test_sensitive_flag(self):
        f = Field.str(sensitive=True)
        assert f.sensitive is True


class TestFieldDecodeEnv:
    """Tests for Field.decode_env()."""

    def test_decodes_present_value(self):
        f = Field.int(default=0)
        assert f.decode_env({"PORT": "8080"}, "PORT") == 8080

    def test_returns_none_when_missing(self):
        f = Field.int(default=0)
        assert f.decode_env({}, "PORT") is None

    def test_returns_none_when_blank(self):
        f = Field.str(default="x")
        assert f.decode_env({"NAME": "   "}, "NAME") is None

    def test_returns_none_when_empty(self):
        f = Field.str(default="x")
        assert f.decode_env({"NAME": ""}, "NAME") is None

    def test_strips_whitespace(self):
        f = Field.str(default="x")
        assert f.decode_env({"NAME": "  hello  "}, "NAME") == "hello"

    def test_raises_decode_error_on_invalid(self):
        f = Field.int(default=0)
        with pytest.raises(DecodeError, match="Cannot decode 'abc' as int"):
            f.decode_env({"PORT": "abc"}, "PORT")

    def test_bool_field(self):
        f = Field.bool(default=False)
        assert f.decode_env({"DEBUG": "true"}, "DEBUG") is True

    def test_path_field(self):
        f = Field.path()
        result = f.decode_env({"DIR": "/tmp/data"}, "DIR")
        assert result == Path("/tmp/data")

    def test_json_field(self):
        f = Field.json()
        result = f.decode_env({"META": '{"k": "v"}'}, "META")
        assert result == {"k": "v"}


class TestFieldRepr:
    """Tests for Field.__repr__."""

    def test_with_default(self):
        f = Field.int(default=42)
        r = repr(f)
        assert "decode_int" in r
        assert "42" in r

    def test_without_default(self):
        f = Field.str()
        r = repr(f)
        assert "decode_str" in r
        assert "default" not in r
