from __future__ import annotations

from typing import Annotated, Any, get_type_hints
from unittest.mock import patch

from pydantic import Field
import pytest

from hipr import ConfigurableIndicator, Hyper, MakeableModel, configurable
from hipr.extraction import (
  _extract_hyper_field_info,
  _get_type_hints_with_fallback,
)


# Extract internal _HyperMarker for testing
# We get the class _HyperMarker from the metadata
def _helper() -> Hyper[int]: ...


_HyperMarker = get_type_hints(_helper, include_extras=True)["return"].__metadata__[0]


def test_configurable_indicator_manual_usage():
  """Test manual instantiation and usage of ConfigurableIndicator."""

  @configurable
  def my_func(x: Hyper[int] = 1) -> int:
    return x

  # configurable returns ConfigurableIndicator now.
  # To test manual usage, we extract the underlying function and config.
  original_fn = my_func._fn
  config_cls = my_func.Config

  indicator = ConfigurableIndicator(original_fn, config_cls)

  # Test __call__
  assert indicator(x=5) == 5

  # Test __get__ (descriptor protocol)
  class MyClass:
    method = indicator

  obj = MyClass()
  bound_method = obj.method
  assert isinstance(bound_method, ConfigurableIndicator)
  # Accessing on class should return the indicator itself
  assert MyClass.method is indicator


def test_get_type_hints_name_error_on_class():
  """Test fallback logic when get_type_hints raises NameError for a class."""

  class MyClass:
    pass

  # Mock get_type_hints to raise NameError on the first call
  # Note: we patch where it is used in hipr.extraction

  call_count = 0
  original_get_type_hints = get_type_hints

  def side_effect(*args, **kwargs):
    nonlocal call_count
    call_count += 1
    if call_count == 1:
      raise NameError("Mocked NameError")
    return original_get_type_hints(*args, **kwargs)

  with patch("hipr.extraction.get_type_hints", side_effect=side_effect):
    hints = _get_type_hints_with_fallback(MyClass)

  assert hints == {}
  assert call_count == 2


def test_nested_config_instantiation_failure():
  """Test error handling when nested config instantiation fails."""

  class BrokenConfig(MakeableModel[Any]):
    def make(self) -> Any:
      return None

    def __init__(self, **data: Any):
      raise RuntimeError("Intentional failure")

  from hipr import DEFAULT

  # Construct a valid Hyper annotation manually
  # Annotated[BrokenConfig, _HyperMarker]
  annotation = Annotated[BrokenConfig, _HyperMarker]

  with pytest.raises(TypeError, match="Failed to instantiate nested config type"):
    _extract_hyper_field_info(annotation, DEFAULT, "param")


def test_existing_field_info_in_metadata():
  """Test when FieldInfo is already present in Annotated metadata."""

  field = Field(description="foo")

  # Manually construct Annotated[int, _HyperMarker, field]
  annotation = Annotated[int, _HyperMarker, field]

  res = _extract_hyper_field_info(annotation, 10, "param")

  assert res is not None
  inner_type, field_info = res
  assert inner_type is int
  assert field_info is field
