from __future__ import annotations

import attrs
import pytest

from codeflash_core.danom.result import Err, Ok, Result


class TestOk:
    """Tests for Ok variant of Result."""

    def test_construction_with_value(self):
        """
        Ok wraps the given value.
        """
        ok = Ok(42)

        assert 42 == ok.inner

    def test_construction_default_none(self):
        """
        Ok defaults to None when no value is given.
        """
        ok = Ok()

        assert ok.inner is None

    def test_is_ok_returns_true(self):
        """
        is_ok returns True for Ok.
        """
        assert Ok(1).is_ok() is True

    def test_unwrap_returns_inner(self):
        """
        unwrap returns the wrapped value.
        """
        assert "hello" == Ok("hello").unwrap()

    def test_unwrap_none_value(self):
        """
        unwrap returns None when Ok wraps None.
        """
        assert Ok(None).unwrap() is None

    def test_map_applies_func(self):
        """
        map applies the function to the inner value and wraps in Ok.
        """
        result = Ok(3).map(lambda x: x * 2)

        assert Ok(6) == result

    def test_map_with_extra_args(self):
        """
        map passes extra positional and keyword args to the function.
        """
        result = Ok(10).map(lambda x, y, z=0: x + y + z, 5, z=3)

        assert Ok(18) == result

    def test_map_err_is_noop(self):
        """
        map_err returns self unchanged for Ok.
        """
        ok = Ok(42)
        result = ok.map_err(lambda e: "transformed")

        assert result is ok

    def test_and_then_returns_func_result(self):
        """
        and_then applies the function and returns its raw result.
        """
        result = Ok(5).and_then(lambda x: Ok(x + 1))

        assert Ok(6) == result

    def test_and_then_with_extra_args(self):
        """
        and_then passes extra positional and keyword args to the function.
        """
        result = Ok(1).and_then(lambda x, y, z=0: Ok(x + y + z), 2, z=3)

        assert Ok(6) == result

    def test_and_then_can_return_err(self):
        """
        and_then can return an Err from the function.
        """
        result = Ok(5).and_then(lambda x: Err("fail"))

        assert Err("fail") == result

    def test_or_else_is_noop(self):
        """
        or_else returns self unchanged for Ok.
        """
        ok = Ok(42)
        result = ok.or_else(lambda e: Ok(0))

        assert result is ok


class TestErr:
    """Tests for Err variant of Result."""

    def test_construction_with_error(self):
        """
        Err wraps the given error value.
        """
        err = Err("bad")

        assert "bad" == err.error

    def test_construction_default_none(self):
        """
        Err defaults to None error when no value is given.
        """
        err = Err()

        assert err.error is None

    def test_is_ok_returns_false(self):
        """
        is_ok returns False for Err.
        """
        assert Err("x").is_ok() is False

    def test_unwrap_raises_wrapped_exception(self):
        """
        unwrap re-raises the wrapped Exception.
        """
        exc = ValueError("boom")

        with pytest.raises(ValueError, match="boom"):
            Err(exc).unwrap()

    def test_unwrap_non_exception_raises_valueerror(self):
        """
        unwrap raises ValueError when the error is not an Exception.
        """
        with pytest.raises(
            ValueError, match="Err does not have a caught error"
        ):
            Err("not an exception").unwrap()

    def test_unwrap_none_error_raises_valueerror(self):
        """
        unwrap raises ValueError when the error is None.
        """
        with pytest.raises(
            ValueError, match="Err does not have a caught error"
        ):
            Err(None).unwrap()

    def test_map_is_noop(self):
        """
        map returns self unchanged for Err.
        """
        err = Err("fail")
        result = err.map(lambda x: x * 2)

        assert result is err

    def test_map_err_applies_func(self):
        """
        map_err applies the function to the error value.
        """
        result = Err("bad").map_err(lambda e: e.upper())

        assert Err("BAD") == result

    def test_map_err_with_extra_args(self):
        """
        map_err passes extra positional and keyword args to the function.
        """
        result = Err(10).map_err(lambda e, x, y=0: e + x + y, 5, y=3)

        assert Err(18) == result

    def test_and_then_is_noop(self):
        """
        and_then returns self unchanged for Err.
        """
        err = Err("fail")
        result = err.and_then(lambda x: Ok(x + 1))

        assert result is err

    def test_or_else_applies_func(self):
        """
        or_else applies the function to the error and returns its result.
        """
        result = Err("fail").or_else(lambda e: Ok("recovered"))

        assert Ok("recovered") == result

    def test_or_else_with_extra_args(self):
        """
        or_else passes extra positional and keyword args to the function.
        """
        result = Err("x").or_else(
            lambda e, suffix, sep="-": Ok(e + sep + suffix), "y", sep="+"
        )

        assert Ok("x+y") == result

    def test_or_else_can_return_err(self):
        """
        or_else can return another Err from the function.
        """
        result = Err("first").or_else(lambda e: Err("second"))

        assert Err("second") == result

    def test_input_args_stored(self):
        """
        input_args are stored on the Err instance.
        """
        args = (("a", "b"), {"key": "val"})
        err = Err("fail", input_args=args)

        assert args == err.input_args

    def test_input_args_default_empty_tuple(self):
        """
        input_args defaults to an empty tuple.
        """
        assert () == Err("fail").input_args

    def test_traceback_stored(self):
        """
        traceback string is stored on the Err instance.
        """
        err = Err("fail", traceback="line 1\nline 2")

        assert "line 1\nline 2" == err.traceback

    def test_traceback_default_empty_string(self):
        """
        traceback defaults to an empty string.
        """
        assert "" == Err("fail").traceback


class TestErrEquality:
    """Tests for Err.__eq__ and __hash__."""

    def test_equal_errs_with_same_exception_type_and_message(self):
        """
        Two Errs with same exception type, message, and input_args are equal.
        """
        assert Err(ValueError("x")) == Err(ValueError("x"))

    def test_unequal_errs_different_message(self):
        """
        Errs with different messages are not equal.
        """
        assert Err(ValueError("x")) != Err(ValueError("y"))

    def test_unequal_errs_different_type(self):
        """
        Errs with different exception types are not equal.
        """
        assert Err(ValueError("x")) != Err(TypeError("x"))

    def test_unequal_errs_different_input_args(self):
        """
        Errs with different input_args are not equal.
        """
        err1 = Err("x", input_args=(("a",), {}))
        err2 = Err("x", input_args=(("b",), {}))

        assert err1 != err2

    def test_err_not_equal_to_non_err(self):
        """
        Err is not equal to a non-Err object.
        """
        assert Err("x") != "x"
        assert Err(42) != 42

    def test_equal_errs_with_string_errors(self):
        """
        Two Errs with identical string errors are equal.
        """
        assert Err("fail") == Err("fail")

    def test_hash_consistent_with_equality(self):
        """
        Equal Err instances produce the same hash.
        """
        err1 = Err(ValueError("x"))
        err2 = Err(ValueError("x"))

        assert hash(err1) == hash(err2)

    def test_hash_differs_for_unequal(self):
        """
        Different Err instances are unlikely to share a hash.
        """
        assert hash(Err("a")) != hash(Err("b"))


class TestOkEquality:
    """Tests for Ok equality and hashing (attrs-generated)."""

    def test_equal_ok_values(self):
        """
        Two Ok instances with the same inner value are equal.
        """
        assert Ok(42) == Ok(42)

    def test_unequal_ok_values(self):
        """
        Two Ok instances with different inner values are not equal.
        """
        assert Ok(1) != Ok(2)

    def test_ok_not_equal_to_err(self):
        """
        Ok and Err are never equal.
        """
        assert Ok(1) != Err(1)

    def test_ok_not_equal_to_plain_value(self):
        """
        Ok is not equal to a plain unwrapped value.
        """
        assert Ok(42) != 42

    def test_hash_consistent_with_equality(self):
        """
        Equal Ok instances produce the same hash.
        """
        assert hash(Ok("a")) == hash(Ok("a"))

    def test_ok_usable_in_sets(self):
        """
        Ok instances can be stored in a set.
        """
        s = {Ok(1), Ok(1), Ok(2)}

        assert 2 == len(s)


class TestResultStaticMethods:
    """Tests for Result.unit, result_is_ok, and result_unwrap."""

    def test_unit_wraps_in_ok(self):
        """
        Result.unit wraps a value in Ok.
        """
        result = Result.unit(99)

        assert Ok(99) == result

    def test_result_is_ok_for_ok(self):
        """
        result_is_ok returns True for an Ok.
        """
        assert Result.result_is_ok(Ok(1)) is True

    def test_result_is_ok_for_err(self):
        """
        result_is_ok returns False for an Err.
        """
        assert Result.result_is_ok(Err("fail")) is False

    def test_result_unwrap_ok(self):
        """
        result_unwrap returns the inner value for Ok.
        """
        assert 42 == Result.result_unwrap(Ok(42))

    def test_result_unwrap_err_raises(self):
        """
        result_unwrap raises for an Err with an Exception.
        """
        with pytest.raises(RuntimeError, match="boom"):
            Result.result_unwrap(Err(RuntimeError("boom")))


class TestNestedResult:
    """Tests for nested Result values."""

    def test_ok_wrapping_ok(self):
        """
        Ok can wrap another Ok.
        """
        nested = Ok(Ok(42))

        assert Ok(42) == nested.unwrap()
        assert 42 == nested.unwrap().unwrap()

    def test_ok_wrapping_err(self):
        """
        Ok can wrap an Err.
        """
        nested = Ok(Err("inner"))
        inner = nested.unwrap()

        assert False is inner.is_ok()
        assert "inner" == inner.error

    def test_map_on_nested_ok(self):
        """
        map on a nested Ok applies the function to the inner Result.
        """
        nested = Ok(Ok(5))
        result = nested.map(lambda inner_ok: inner_ok.map(lambda x: x * 2))

        assert Ok(Ok(10)) == result


class TestErrTracebackExtraction:
    """Tests for Err traceback detail extraction."""

    def test_details_populated_from_exception_traceback(self):
        """
        details list is populated when error is an Exception with a traceback.
        """
        try:
            msg = "test error"
            raise ValueError(msg)  # noqa: TRY301
        except ValueError as exc:
            err = Err(exc)

        assert len(err.details) > 0
        assert "file" in err.details[0]
        assert "func" in err.details[0]
        assert "line_no" in err.details[0]
        assert "locals" in err.details[0]

    def test_details_empty_for_non_exception(self):
        """
        details list is empty when error is not an Exception.
        """
        err = Err("just a string")

        assert [] == err.details

    def test_details_empty_for_exception_without_traceback(self):
        """
        details list is empty for an Exception created without raising.
        """
        err = Err(ValueError("no traceback"))

        assert [] == err.details


class TestErrImmutability:
    """Tests for frozen behavior of Ok and Err."""

    def test_ok_is_frozen(self):
        """
        Ok instances cannot be mutated.
        """
        ok = Ok(42)

        with pytest.raises(attrs.exceptions.FrozenInstanceError):
            ok.inner = 99  # type: ignore[misc]

    def test_err_is_frozen(self):
        """
        Err instances cannot be mutated.
        """
        err = Err("fail")

        with pytest.raises(attrs.exceptions.FrozenInstanceError):
            err.error = "other"  # type: ignore[misc]


class TestErrValidators:
    """Tests for Err field validators."""

    def test_input_args_must_be_tuple(self):
        """
        input_args rejects non-tuple values.
        """
        with pytest.raises(TypeError):
            Err("fail", input_args=["not", "a", "tuple"])  # type: ignore[arg-type]

    def test_traceback_must_be_string(self):
        """
        traceback rejects non-string values.
        """
        with pytest.raises(TypeError):
            Err("fail", traceback=123)  # type: ignore[arg-type]
