[ tests/test_configured.py: ]

import pytest
import sys
import json
from dataclasses import dataclass
from typing import List, Dict, Union, Annotated, Optional

from pico_ioc import (
    init,
    configured,
    scope,
    component,
    qualifier,
    Qualifier,
    PicoContainer,
    DictSource,
    JsonTreeSource,
    YamlTreeSource,
    Discriminator,
    ComponentCreationError,
    ConfigurationError,
)

@dataclass
class DBSettings:
    host: str
    port: int
    user: Optional[str] = "default"

@dataclass
class RedisSettings:
    url: str

@dataclass
class CacheSettings:
    ttl: int
    redis: RedisSettings

@dataclass
class FeatureConfig:
    features: List[str]

@dataclass
class User:
    name: str

@dataclass
class UserList:
    users: List[User]

@dataclass
class HeadersConfig:
    headers: Dict[str, str]

class SimpleService:
    def __init__(self, endpoint: str, timeout: int = 10):
        self.endpoint = endpoint
        self.timeout = timeout

@dataclass
class Cat:
    name: str
    lives: int = 9

@dataclass
class Dog:
    name: str
    breed: str

@dataclass
class PetOwnerDefault:
    pet: Union[Cat, Dog]

@dataclass
class PetOwnerCustom:
    pet: Annotated[Union[Cat, Dog], Discriminator("animal_type")]

@configured(target=DBSettings, prefix="db")
class ConfiguredDBSettings:
    pass

@configured(target=CacheSettings, prefix="cache")
class ConfiguredCacheSettings:
    pass

@configured(target=FeatureConfig, prefix="app")
class ConfiguredFeatureConfig:
    pass

@configured(target=UserList, prefix="user_config")
class ConfiguredUserList:
    pass

@configured(target=HeadersConfig, prefix="api")
class ConfiguredHeadersConfig:
    pass

@configured(target=SimpleService, prefix="service")
class ConfiguredSimpleService:
    pass

@configured(target=DBSettings, prefix="db_file")
class ConfiguredDBSettingsFile:
    pass

@configured(target=DBSettings, prefix="db_yaml")
class ConfiguredDBSettingsYaml:
    pass

@configured(target=DBSettings, prefix="db_env")
class ConfiguredDBSettingsEnv:
    pass

@configured(target=DBSettings, prefix="db_ref")
class ConfiguredDBSettingsRef:
    pass

@configured(target=DBSettings, prefix="missing.prefix")
class ConfiguredDBSettingsMissing:
    pass

@configured(target=PetOwnerDefault, prefix="pet_owner_default")
class ConfiguredPetOwnerDefault:
    pass

@configured(target=PetOwnerCustom, prefix="pet_owner_custom")
class ConfiguredPetOwnerCustom:
    pass

@pytest.fixture
def temp_json_file(tmp_path):
    file_path = tmp_path / "config.json"
    def _create(data: dict):
        with open(file_path, "w", encoding="utf-8") as f:
            json.dump(data, f)
        return str(file_path)
    return _create

@pytest.fixture
def temp_yaml_file(tmp_path):
    file_path = tmp_path / "config.yaml"
    def _create(data: str):
        with open(file_path, "w", encoding="utf-8") as f:
            f.write(data)
        return str(file_path)
    return _create

def test_configured_basic_dataclass():
    config_data = {"db": {"host": "localhost", "port": 5432}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    settings = container.get(DBSettings)
    assert isinstance(settings, DBSettings)
    assert settings.host == "localhost"
    assert settings.port == 5432
    assert settings.user == "default"

def test_configured_nested_dataclass():
    config_data = {"cache": {"ttl": 3600, "redis": {"url": "redis://host:6379"}}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    settings = container.get(CacheSettings)
    assert isinstance(settings, CacheSettings)
    assert settings.ttl == 3600
    assert isinstance(settings.redis, RedisSettings)
    assert settings.redis.url == "redis://host:6379"

def test_configured_list_of_primitives():
    config_data = {"app": {"features": ["foo", "bar", "baz"]}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    config = container.get(FeatureConfig)
    assert isinstance(config, FeatureConfig)
    assert config.features == ["foo", "bar", "baz"]

def test_configured_list_of_dataclasses():
    config_data = {"user_config": {"users": [{"name": "Alice"}, {"name": "Bob"}]}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    config = container.get(UserList)
    assert isinstance(config, UserList)
    assert len(config.users) == 2
    assert isinstance(config.users[0], User)
    assert config.users[0].name == "Alice"
    assert config.users[1].name == "Bob"

def test_configured_dict():
    config_data = {"api": {"headers": {"X-Auth": "token123", "User-Agent": "pico"}}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    config = container.get(HeadersConfig)
    assert isinstance(config, HeadersConfig)
    assert config.headers == {"X-Auth": "token123", "User-Agent": "pico"}

def test_configured_plain_class_init():
    config_data = {"service": {"endpoint": "http://api.com", "timeout": 5}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    service = container.get(SimpleService)
    assert isinstance(service, SimpleService)
    assert service.endpoint == "http://api.com"
    assert service.timeout == 5

def test_configured_json_tree_source(temp_json_file):
    config_path = temp_json_file({"db_file": {"host": "file.host", "port": 1234, "user": "file_user"}})
    container = init(modules=[__name__], tree_config=(JsonTreeSource(config_path),))
    settings = container.get(DBSettings)
    assert isinstance(settings, DBSettings)
    assert settings.host == "file.host"
    assert settings.port == 1234
    assert settings.user == "file_user"

def test_configured_yaml_tree_source(temp_yaml_file):
    yaml = pytest.importorskip("yaml")
    config_content = """
db_yaml:
  host: yaml.host
  port: 5678
"""
    config_path = temp_yaml_file(config_content)
    container = init(modules=[__name__], tree_config=(YamlTreeSource(config_path),))
    settings = container.get(DBSettings)
    assert isinstance(settings, DBSettings)
    assert settings.host == "yaml.host"
    assert settings.port == 5678

def test_configured_env_interpolation(monkeypatch):
    monkeypatch.setenv("DB_HOST_ENV", "env.host")
    config_data = {"db_env": {"host": "${ENV:DB_HOST_ENV}", "port": 9999}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    settings = container.get(DBSettings)
    assert settings.host == "env.host"
    assert settings.port == 9999

def test_configured_ref_interpolation():
    config_data = {"defaults": {"user": "ref_user"}, "db_ref": {"host": "ref.host", "port": 1111, "user": "${ref:defaults.user}"}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    settings = container.get(DBSettings)
    assert settings.host == "ref.host"
    assert settings.port == 1111
    assert settings.user == "ref_user"

def test_configured_error_on_missing_prefix():
    container = init(modules=[__name__], tree_config=(DictSource({}),))
    with pytest.raises(ComponentCreationError) as e:
        container.get(DBSettings)
    assert isinstance(e.value.cause, ConfigurationError)
    assert "Missing config prefix: missing.prefix" in str(e.value.cause)

def test_configured_union_default_discriminator():
    config_data = {"pet_owner_default": {"pet": {"$type": "Cat", "name": "Fluffy"}}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    owner = container.get(PetOwnerDefault)
    assert isinstance(owner.pet, Cat)
    assert owner.pet.name == "Fluffy"

def test_configured_union_custom_discriminator():
    config_data = {"pet_owner_custom": {"pet": {"animal_type": "Dog", "name": "Buddy", "breed": "Labrador"}}}
    container = init(modules=[__name__], tree_config=(DictSource(config_data),))
    owner = container.get(PetOwnerCustom)
    assert isinstance(owner.pet, Dog)
    assert owner.pet.name == "Buddy"
    assert owner.pet.breed == "Labrador"

def test_configured_selects_existing_and_longest_prefix():
    @configured(target=DBSettings, prefix="db")
    class CfgA:
        pass
    @configured(target=DBSettings, prefix="db_prod")
    class CfgB:
        pass
    mod = sys.modules[__name__]
    setattr(mod, "CfgA", CfgA)
    setattr(mod, "CfgB", CfgB)
    container = init([__name__], tree_config=(DictSource({"db_prod": {"host": "h", "port": 1}}),))
    s = container.get(DBSettings)
    assert s.host == "h"

def test_configured_sets_config_metadata():
    data = {"db": {"host": "x", "port": 1}}
    c = init([__name__], tree_config=(DictSource(data),))
    s = c.get(DBSettings)
    meta = getattr(s, "_pico_meta")
    assert meta["config_prefix"] == "db"
    assert "config_hash" in meta
    assert tuple(meta["config_source_order"])

def test_union_wrong_discriminator_raises_clear_error():
    data = {"pet_owner_default": {"pet": {"$type": "Bird", "name": "Kiwi"}}}
    c = init([__name__], tree_config=(DictSource(data),))
    with pytest.raises(ComponentCreationError) as e:
        c.get(PetOwnerDefault)
    assert "Discriminator $type did not match" in str(e.value.cause)

def test_configured_typed_dict_and_type_errors():
    @dataclass
    class D:
        m: Dict[str, int]
    @configured(target=D, prefix="d")
    class C:
        pass
    mod = sys.modules[__name__]
    setattr(mod, "D", D)
    setattr(mod, "C", C)
    c = init([__name__], tree_config=(DictSource({"d": {"m": {"a": "1"}}}),))
    assert c.get(D).m == {"a": 1}

def test_string_injection_resolves_by_pico_name_or_classname():
    @component(name="SpecialWidget")
    class W:
        pass
    @component
    class Needs:
        def __init__(self, w: "SpecialWidget"):
            self.w = w
    mod = sys.modules[__name__]
    setattr(mod, "W", W)
    setattr(mod, "Needs", Needs)
    c = init([__name__])
    assert isinstance(c.get(Needs).w, W)

[ tests/test_container_context.py: ]

# tests/test_container_context.py
import types
import pytest
from pico_ioc import init, PicoContainer

def _empty_module(name: str = "ctx_mod"):
    return types.ModuleType(name)

def test_container_id_unique():
    c1 = init(_empty_module("m1"))
    c2 = init(_empty_module("m2"))
    assert isinstance(c1.container_id, str) and c1.container_id
    assert isinstance(c2.container_id, str) and c2.container_id
    assert c1.container_id != c2.container_id

def test_as_current_context_manager():
    c = init(_empty_module("m_ctx"))
    assert PicoContainer.get_current() is None
    with c.as_current():
        cur = PicoContainer.get_current()
        assert cur is c
        assert PicoContainer.get_current_id() == c.container_id
    assert PicoContainer.get_current() is None
    assert PicoContainer.get_current_id() is None

def test_activate_and_deactivate_methods():
    c = init(_empty_module("m_ctx2"))
    token = c.activate()
    try:
        assert PicoContainer.get_current() is c
        assert PicoContainer.get_current_id() == c.container_id
    finally:
        c.deactivate(token)
    assert PicoContainer.get_current() is None

def test_all_containers_registry_contains_instances():
    c1 = init(_empty_module("m3"))
    c2 = init(_empty_module("m4"))
    reg = PicoContainer.all_containers()
    assert c1.container_id in reg and reg[c1.container_id] is c1
    assert c2.container_id in reg and reg[c2.container_id] is c2

def test_stats_contains_container_id_and_profiles():
    c = init(_empty_module("m_stats"), profiles=("dev",))
    st = c.stats()
    assert st["container_id"] == c.container_id
    assert tuple(st["profiles"]) == ("dev",)
    assert isinstance(st["total_resolves"], int)
    assert isinstance(st["cache_hits"], int)
    assert "registered_components" in st

[ tests/test_container_runtime.py: ]

import pytest
import asyncio
from pico_ioc import init, component, PicoContainer, ScopeError

@component
class SimpleA:
    pass

@component
class SimpleB:
    pass

@component
class NeedsSimpleA:
    def __init__(self, a: SimpleA):
        self.a = a

class RedisCache: pass
class InMemoryCache: pass

# Mock components for profile test
@component(name="Cache")
class ProdCache(RedisCache): pass

@component(name="Cache")
class DevCache(InMemoryCache): pass

@component
class AsyncService:
    def __init__(self):
        self.container_id = PicoContainer.get_current_id()
        
def test_container_id_uniqueness():
    c1 = init(modules=[__name__])
    c2 = init(modules=[__name__])
    assert c1.container_id != c2.container_id
    c1.shutdown()
    c2.shutdown()

def test_container_context_isolation():
    c1 = init(modules=[__name__], profiles=("prod",), container_id="test-prod")
    c2 = init(modules=[__name__], profiles=("dev",), container_id="test-dev")
    
    with c1.as_current():
        assert PicoContainer.get_current_id() == c1.container_id
        service1 = c1.get(NeedsSimpleA)
    
    with c2.as_current():
        assert PicoContainer.get_current_id() == c2.container_id
        service2 = c2.get(NeedsSimpleA)
    
    assert service1 is not service2
    c1.shutdown()
    c2.shutdown()

def test_nested_context_managers():
    c1 = init(modules=[__name__])
    c2 = init(modules=[__name__])
    
    with c1.as_current():
        assert PicoContainer.get_current() is c1
        
        with c2.as_current():
            assert PicoContainer.get_current() is c2
        
        assert PicoContainer.get_current() is c1
        
    c1.shutdown()
    c2.shutdown()

def test_container_stats():
    container = init(modules=[__name__]) 
    
    with container.as_current():
        container.get(SimpleA)
        container.get(SimpleA)
        container.get(SimpleB)
    
    stats = container.stats()
    
    assert stats["total_resolves"] == 2
    assert stats["cache_hits"] == 1
    assert stats["cache_hit_rate"] == 1 / 3
    container.shutdown()

def test_container_shutdown_cleanup():
    container = init(modules=[__name__], container_id="test-shutdown")
    assert "test-shutdown" in PicoContainer.all_containers()
    
    container.shutdown()
    assert "test-shutdown" not in PicoContainer.all_containers()

async def async_helper_function():
    current = PicoContainer.get_current()
    if not current:
        raise RuntimeError("No active container context")
    service = await current.aget(AsyncService)
    return service

@pytest.mark.asyncio
async def test_async_context_preservation():
    container = init(modules=[__name__])
    
    result_service = None
    with container.as_current():
        result_service = await async_helper_function()
        
    assert result_service is not None
    assert result_service.container_id == container.container_id
    container.shutdown()
[ tests/test_event_bus.py: ]

# tests/test_event_bus.py
import asyncio
import threading
import pytest
from typing import List
from pico_ioc import EventBus, ExecPolicy, ErrorPolicy, Event, subscribe, AutoSubscriberMixin

class MyEvent(Event):
    def __init__(self, value: int):
        self.value = value

def test_subscribe_and_publish_sync():
    bus = EventBus()
    seen: List[int] = []
    def handler(evt: MyEvent):
        seen.append(evt.value)
    bus.subscribe(MyEvent, handler)
    bus.publish_sync(MyEvent(42))
    assert seen == [42]

def test_priority_order_sync():
    bus = EventBus()
    seen: List[str] = []
    def h_low(evt: MyEvent):
        seen.append("low")
    def h_high(evt: MyEvent):
        seen.append("high")
    bus.subscribe(MyEvent, h_low, priority=0)
    bus.subscribe(MyEvent, h_high, priority=10)
    bus.publish_sync(MyEvent(1))
    assert seen == ["high", "low"]

@pytest.mark.asyncio
async def test_once_subscription_and_unsubscribe():
    bus = EventBus()
    seen: List[int] = []
    def handler(evt: MyEvent):
        seen.append(evt.value)
    bus.subscribe(MyEvent, handler, once=True)
    await bus.publish(MyEvent(1))
    await bus.publish(MyEvent(2))
    assert seen == [1]

@pytest.mark.asyncio
async def test_async_handler_inline_and_task_policy():
    bus = EventBus()
    seen: List[str] = []
    async def h_inline(evt: MyEvent):
        await asyncio.sleep(0)
        seen.append("inline")
    async def h_task(evt: MyEvent):
        await asyncio.sleep(0)
        seen.append("task")
    bus.subscribe(MyEvent, h_inline, priority=0, policy=ExecPolicy.INLINE)
    bus.subscribe(MyEvent, h_task, priority=0, policy=ExecPolicy.TASK)
    await bus.publish(MyEvent(0))
    assert set(seen) == {"inline", "task"}

@pytest.mark.asyncio
async def test_threadpool_policy():
    bus = EventBus()
    seen: List[str] = []
    def h_tp(evt: MyEvent):
        seen.append("tp")
    bus.subscribe(MyEvent, h_tp, policy=ExecPolicy.THREADPOOL)
    await bus.publish(MyEvent(0))
    assert seen == ["tp"]

@pytest.mark.asyncio
async def test_worker_queue_post_from_thread():
    bus = EventBus(max_queue_size=10)
    seen: List[int] = []
    def handler(evt: MyEvent):
        seen.append(evt.value)
    bus.subscribe(MyEvent, handler)
    await bus.start_worker()
    def producer():
        for i in range(3):
            bus.post(MyEvent(i))
    t = threading.Thread(target=producer)
    t.start()
    t.join()
    await asyncio.sleep(0.05)
    await bus.stop_worker()
    assert seen == [0, 1, 2]

@pytest.mark.asyncio
async def test_error_policy_raise():
    bus = EventBus(error_policy=ErrorPolicy.RAISE)
    def bad(evt: MyEvent):
        raise RuntimeError("boom")
    bus.subscribe(MyEvent, bad)
    with pytest.raises(Exception):
        await bus.publish(MyEvent(0))

def test_auto_subscriber_mixin():
    seen: List[int] = []
    class S(AutoSubscriberMixin):
        @subscribe(MyEvent, priority=5)
        def handle(self, evt: MyEvent):
            seen.append(evt.value)
    bus = EventBus()
    s = S()
    s._pico_autosubscribe(bus)
    bus.publish_sync(MyEvent(7))
    assert seen == [7]

[ tests/test_pico_extends.py: ]

import pytest
import os
import types
import asyncio
import pickle
import logging
from dataclasses import dataclass
from typing import List, Any, Callable, Optional
import pico_ioc.event_bus
from pico_ioc import (
    init, component, factory, provides, configuration, conditional, scope,
    cleanup, configure, health, intercepted_by, lazy,
    PicoContainer, MethodInterceptor, MethodCtx, ScopeError,
    InvalidBindingError, CircularDependencyError, ComponentCreationError,
    ProviderNotFoundError, ConfigurationError, SerializationError,
    Event, subscribe, AutoSubscriberMixin, EventBus, EventBusClosedError
)


log_capture = []

class ListLogHandler(logging.Handler):
    def emit(self, record):
        log_capture.append(self.format(record))


test_logger = logging.getLogger("TestCoverageLogger")
test_logger.handlers.clear()
test_logger.addHandler(ListLogHandler())
test_logger.setLevel(logging.INFO)

@pytest.fixture(autouse=True)
def reset_logging_capture():
    log_capture.clear()

@component
class MissingDependency:
    pass

@component
class NeedsMissingComponent:
    def __init__(self, missing: MissingDependency):
        self.missing = missing

@component
class CircularA:
    def __init__(self, b: "CircularB"):
        self.b = b

@component
class CircularB:
    def __init__(self, a: CircularA):
        self.a = a

@component
class FailingComponent:
    def __init__(self):
        raise ValueError("Creation failure")

@configuration
@dataclass
class RequiredConfig:
    REQUIRED_KEY: str

class Widget:
    pass

@factory
class WidgetFactory:
    @provides(Widget)
    def build_widget(self) -> Widget:
        test_logger.info("Widget_Factory: Creating Widget")
        return Widget()

@component
class ConfiguredComponent:
    def __init__(self):
        self.configured = False
        self.widget = None
        test_logger.info("ConfiguredComponent: __init__")
    
    @configure
    def setup(self, widget: Widget):
        self.widget = widget
        self.configured = True
        test_logger.info("ConfiguredComponent: @configure called")

@component
class AsyncResource:
    def __init__(self):
        self.closed = False
        test_logger.info("AsyncResource: Created")
    
    @cleanup
    async def async_close(self):
        await asyncio.sleep(0)
        self.closed = True
        test_logger.info("AsyncResource: @cleanup async called")

@component
@conditional(require_env=("MY_TEST_VAR",))
class EnvConditionalComponent:
    pass

def check_predicate():
    return os.environ.get("PREDICATE_SWITCH") == "ON"

@component
@conditional(predicate=check_predicate)
class PredicateConditionalComponent:
    pass

@component
class HealthyComponent:
    @health
    def check_db(self):
        test_logger.info("Health: check_db OK")
        return True

    @health
    def check_api(self):
        test_logger.info("Health: check_api FAILED")
        raise ValueError("API not available")

@component
class AuditInterceptor(MethodInterceptor):
    def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
        test_logger.info(f"AUDIT_ASYNC - Entering: {ctx.name}")
        res = call_next(ctx)
        test_logger.info(f"AUDIT_ASYNC - Exiting: {ctx.name}")
        return res

@component
class AsyncAuditedService:
    @intercepted_by(AuditInterceptor)
    async def do_async_work(self, val: int) -> int:
        test_logger.info("AsyncAuditedService: working...")
        await asyncio.sleep(0)
        return val * 2

@component
@lazy
class MyLazyComponentForPickle:
    def __init__(self):
        self.value = 42

class MyTestEvent(Event):
    def __init__(self, msg: str):
        self.msg = msg

@component
class MyTestSubscriber(AutoSubscriberMixin):
    def __init__(self):
        self.received: List[str] = []

    @subscribe(MyTestEvent)
    def handle_event(self, evt: MyTestEvent):
        test_logger.info(f"Event received: {evt.msg}")
        self.received.append(evt.msg)


test_module = types.ModuleType("test_coverage_module")
all_definitions = [
    NeedsMissingComponent, MissingDependency, CircularA, CircularB, FailingComponent,
    RequiredConfig, Widget, WidgetFactory, ConfiguredComponent, AsyncResource,
    EnvConditionalComponent, PredicateConditionalComponent, HealthyComponent,
    AuditInterceptor, AsyncAuditedService, MyLazyComponentForPickle,
    MyTestEvent, MyTestSubscriber
]
for item in all_definitions:
    if hasattr(item, "__name__"):
        setattr(test_module, item.__name__, item)


def test_invalid_binding_error_on_init():
    mod = types.ModuleType("fail_mod")
    setattr(mod, NeedsMissingComponent.__name__, NeedsMissingComponent)
    
    with pytest.raises(InvalidBindingError) as e:
        init(mod, validate_only=True)
    
    assert "NeedsMissingComponent constructor depends on MissingDependency" in str(e.value)

def test_circular_dependency_error():
    container = init(test_module)
    with pytest.raises(ComponentCreationError) as e:
        container.get(CircularA)
    
    root_cause = e.value
    while hasattr(root_cause, 'cause') and root_cause.cause is not None:
            root_cause = root_cause.cause
    
    assert isinstance(root_cause, CircularDependencyError)
    assert "CircularA" in str(e.value)

def test_component_creation_error():
    container = init(test_module)
    with pytest.raises(ComponentCreationError) as e:
        container.get(FailingComponent)
    
    assert "Failed to create component for key: FailingComponent" in str(e.value)
    assert "ValueError: Creation failure" in str(e.value)

def test_provider_not_found_error():
    container = init(types.ModuleType("empty_mod"))
    
    with pytest.raises(ProviderNotFoundError) as e_str:
        container.get("non_existent_key")
    assert "Provider not found for key: non_existent_key" in str(e_str.value)

    class NonExistentClass: pass
    with pytest.raises(ProviderNotFoundError) as e_type:
        container.get(NonExistentClass)
    assert "Provider not found for key: NonExistentClass" in str(e_type.value)

def test_configuration_error_missing_value():
    config_module = types.ModuleType("config_test_mod")
    setattr(config_module, RequiredConfig.__name__, RequiredConfig)
    
    with pytest.raises(ComponentCreationError) as e:
        container = init(config_module, config=())
        container.get(RequiredConfig) 
    
    assert isinstance(e.value.cause, ConfigurationError)
    assert "Missing configuration key: REQUIRED_KEY" in str(e.value.cause)

def test_configuration_error_disallowed_profile():
    with pytest.raises(ConfigurationError) as e:
        init(
            types.ModuleType("empty_mod"),
            profiles=("dev",),
            allowed_profiles=("prod", "test")
        )
    assert "Unknown profiles: ['dev']; allowed: ['prod', 'test']" in str(e.value)

def test_scope_error_unknown_scope():
    container = init(types.ModuleType("empty_mod"))
    with pytest.raises(ScopeError) as e:
        container.activate_scope("unreal_scope", "id-123")
    assert "Unknown scope: unreal_scope" in str(e.value)


def test_factory_and_provides_pattern():
    container = init(test_module)
    widget_instance = container.get(Widget)
    
    assert isinstance(widget_instance, Widget)
    assert "Widget_Factory: Creating Widget" in log_capture

def test_configure_lifecycle_method():
    container = init(test_module)
    
    assert "ConfiguredComponent: __init__" not in log_capture
    
    instance = container.get(ConfiguredComponent)
    
    assert instance.configured is True
    assert isinstance(instance.widget, Widget)
    
    init_index = log_capture.index("ConfiguredComponent: __init__")
    configure_index = log_capture.index("ConfiguredComponent: @configure called")
    assert init_index < configure_index

@pytest.mark.asyncio
async def test_async_cleanup_method():
    container = init(test_module)
    resource = container.get(AsyncResource)
    
    assert resource.closed is False
    assert "AsyncResource: Created" in log_capture
    
    await container.cleanup_all_async()
    
    assert resource.closed is True
    assert "AsyncResource: @cleanup async called" in log_capture


def test_conditional_on_env(monkeypatch):
    monkeypatch.delenv("MY_TEST_VAR", raising=False)
    container_no_env = init(test_module)
    with pytest.raises(ProviderNotFoundError):
        container_no_env.get(EnvConditionalComponent)
        
    monkeypatch.setenv("MY_TEST_VAR", "any_value")
    container_with_env = init(test_module)
    instance = container_with_env.get(EnvConditionalComponent)
    assert isinstance(instance, EnvConditionalComponent)

def test_conditional_on_predicate(monkeypatch):
    monkeypatch.setenv("PREDICATE_SWITCH", "OFF")
    container_false = init(test_module)
    with pytest.raises(ProviderNotFoundError):
        container_false.get(PredicateConditionalComponent)
        
    monkeypatch.setenv("PREDICATE_SWITCH", "ON")
    container_true = init(test_module)
    instance = container_true.get(PredicateConditionalComponent)
    assert isinstance(instance, PredicateConditionalComponent)

def test_health_check_decorator():
    container = init(test_module)
    
    container.get(HealthyComponent)
    
    health_status = container.health_check()
    
    assert "HealthyComponent.check_db" in health_status
    assert health_status["HealthyComponent.check_db"] is True
    
    assert "HealthyComponent.check_api" in health_status
    assert health_status["HealthyComponent.check_api"] is False
    
    assert "Health: check_db OK" in log_capture
    assert "Health: check_api FAILED" in log_capture

@pytest.mark.asyncio
async def test_aop_on_async_method():
    container = init(test_module)
    service = container.get(AsyncAuditedService)
    
    result = await service.do_async_work(10)
    
    assert result == 20
    assert "AUDIT_ASYNC - Entering: do_async_work" in log_capture
    assert "AsyncAuditedService: working..." in log_capture
    assert "AUDIT_ASYNC - Exiting: do_async_work" in log_capture

def test_proxy_serialization_pickle():
    container = init(test_module)
    
    lazy_proxy = container.get(MyLazyComponentForPickle)
    
    intercepted_proxy = container.get(AsyncAuditedService)
    
    try:
        pickled_lazy = pickle.dumps(lazy_proxy)
        pickled_intercepted = pickle.dumps(intercepted_proxy)
        
        unpickled_lazy = pickle.loads(pickled_lazy)
        unpickled_intercepted = pickle.loads(pickled_intercepted)
        
        assert unpickled_lazy.value == 42
        assert isinstance(unpickled_intercepted, AsyncAuditedService)
        
    except (pickle.PicklingError, SerializationError) as e:
        pytest.fail(f"Proxy serialization failed: {e}")

@pytest.mark.asyncio
async def test_event_bus_integration_and_shutdown():
    container = init([test_module, pico_ioc.event_bus])
    
    bus = container.get(EventBus)
    subscriber = container.get(MyTestSubscriber)
    
    assert isinstance(bus, EventBus)
    assert len(subscriber.received) == 0
    assert "Event received: Hello" not in log_capture
    
    await bus.publish(MyTestEvent("Hello"))
    
    assert subscriber.received == ["Hello"]
    assert "Event received: Hello" in log_capture
    
    await container.cleanup_all_async()
    
    with pytest.raises(EventBusClosedError):
        await bus.publish(MyTestEvent("Goodbye"))
        
[ tests/test_pico_integration.py: ]

import pytest
import os
import json
import contextvars
from dataclasses import dataclass
from typing import List, Optional, Annotated, Callable, Any, Protocol
import logging
import time

from pico_ioc import (
    component, factory, provides, configuration, lazy, primary,
    conditional, scope, Qualifier, qualifier, intercepted_by, cleanup,
    init, PicoContainer, MethodInterceptor, MethodCtx,
    EnvSource, FileSource, ScopeProtocol, ContextVarScope, on_missing
)

logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')
log_capture = []

class ListLogHandler(logging.Handler):
    def emit(self, record):
        log_capture.append(self.format(record))

LOGGER = logging.getLogger("pico_ioc")
test_logger = logging.getLogger("TestLogger")
test_logger.addHandler(ListLogHandler())
test_logger.setLevel(logging.INFO)

@pytest.fixture(autouse=True)
def reset_logging_capture():
    log_capture.clear()

@pytest.fixture
def temp_config_file(tmp_path):
    file_path = tmp_path / "config.json"
    def _create(data: dict):
        with open(file_path, "w", encoding="utf-8") as f:
            json.dump(data, f)
        return str(file_path)
    return _create

class ServiceA: pass
class ServiceB: pass
class Database:
    def query(self, sql: str) -> str: return f"Query: {sql}"
class Cache:
    def get(self, key: str) -> Optional[str]: return None
    def set(self, key: str, value: str): pass

@component
class ServiceAImpl(ServiceA): pass

@component
class ServiceBImpl(ServiceB):
    def __init__(self, service_a: ServiceA):
        self.service_a = service_a

@component
@primary
class PostgresDB(Database):
    def query(self, sql: str) -> str:
        test_logger.info("Executing Postgres query")
        return f"Postgres: {sql}"

@component
class MysqlDB(Database):
     def query(self, sql: str) -> str:
        test_logger.info("Executing MySQL query")
        return f"MySQL: {sql}"

@component
@lazy
class LazyComponent:
    def __init__(self):
        test_logger.info("LazyComponent Instantiated!")
        self.created_at = time.time()

@configuration(prefix="APP_")
@dataclass
class AppConfig:
    DEBUG: bool = False
    TIMEOUT: int = 30
    DB_HOST: str = "localhost"

REQUEST_ID_VAR = contextvars.ContextVar("test_request_id", default=None)
request_scope = ContextVarScope(REQUEST_ID_VAR)

@component
@scope("request")
class RequestScopedData:
    def __init__(self):
        self.request_id = REQUEST_ID_VAR.get()
        test_logger.info(f"RequestScopedData created for request: {self.request_id}")

@component
class NeedsScoped:
    def __init__(self, req_data: RequestScopedData):
        self.req_data = req_data

PAYMENT = Qualifier("payment")
NOTIFICATION = Qualifier("notification")

class Sender(Protocol):
    def send(self, msg: str): ...

@component
@qualifier(PAYMENT)
class StripeSender(Sender):
    def send(self, msg: str): test_logger.info(f"Stripe: {msg}")

@component
@qualifier(PAYMENT)
class PaypalSender(Sender):
    def send(self, msg: str): test_logger.info(f"Paypal: {msg}")

@component
@qualifier(NOTIFICATION)
class EmailSender(Sender):
    def send(self, msg: str): test_logger.info(f"Email: {msg}")

@component
class PaymentService:
    def __init__(self, payment_senders: Annotated[List[Sender], PAYMENT]):
        self.senders = payment_senders

@component
class AuditInterceptor(MethodInterceptor):
    def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
        test_logger.info(f"AUDIT - Entering: {ctx.cls.__name__}.{ctx.name}")
        result = call_next(ctx)
        test_logger.info(f"AUDIT - Exiting: {ctx.cls.__name__}.{ctx.name}")
        return result

audit = intercepted_by(AuditInterceptor)

@component
class AuditedService:
    @audit
    def perform_action(self, action_id: int):
        test_logger.info(f"Performing action {action_id}")
        return f"Action {action_id} done"

@component
@conditional(profiles=("prod",))
class RedisCache(Cache):
    def get(self, key: str) -> Optional[str]: return f"Redis:{key}"
    def set(self, key: str, value: str): test_logger.info("Redis SET")

@component
@on_missing(Cache)
class InMemoryCache(Cache):
     def get(self, key: str) -> Optional[str]: return f"Memory:{key}"
     def set(self, key: str, value: str): test_logger.info("Memory SET")

@component
class CacheClient:
    def __init__(self, cache: Cache):
        self.cache = cache

@component
class ResourceHolder:
    def __init__(self):
        self.closed = False
        test_logger.info("ResourceHolder created")

    @cleanup
    def close(self):
        self.closed = True
        test_logger.info("ResourceHolder closed")

test_module = type(os)('test_integration_module')
definitions = [
    ServiceA, ServiceB, Database, Cache, Sender,
    ServiceAImpl, ServiceBImpl, PostgresDB, MysqlDB, LazyComponent,
    AppConfig,
    RequestScopedData, NeedsScoped,
    StripeSender, PaypalSender, EmailSender, PaymentService,
    AuditInterceptor, AuditedService,
    RedisCache, InMemoryCache, CacheClient,
    ResourceHolder
]
for item in definitions:
    if hasattr(item, '__name__'):
        setattr(test_module, item.__name__, item)

test_scopes = {"request": request_scope}

def test_basic_di():
    container = init(test_module)
    instance_b = container.get(ServiceBImpl)
    assert isinstance(instance_b, ServiceBImpl)
    assert isinstance(instance_b.service_a, ServiceAImpl)
    instance_b_2 = container.get(ServiceBImpl)
    assert instance_b is instance_b_2

def test_configuration_injection_defaults():
    container = init(test_module, config=())
    config = container.get(AppConfig)
    assert isinstance(config, AppConfig)
    assert config.DEBUG is False
    assert config.TIMEOUT == 30
    assert config.DB_HOST == "localhost"

def test_configuration_injection_env(monkeypatch):
    monkeypatch.setenv("APP_DEBUG", "true")
    monkeypatch.setenv("APP_TIMEOUT", "60")
    container = init(test_module, config=(EnvSource(prefix="APP_"),))
    config = container.get(AppConfig)
    assert config.DEBUG is True
    assert config.TIMEOUT == 60
    assert config.DB_HOST == "localhost"

def test_configuration_injection_file(temp_config_file):
    config_path = temp_config_file({"APP_DB_HOST": "remote.db", "APP_TIMEOUT": 90})
    container = init(test_module, config=(FileSource(config_path, prefix="APP_"),))
    config = container.get(AppConfig)
    assert config.DB_HOST == "remote.db"
    assert config.TIMEOUT == 90
    assert config.DEBUG is False

def test_configuration_precedence_env_over_file(monkeypatch, temp_config_file):
    monkeypatch.setenv("APP_TIMEOUT", "120")
    config_path = temp_config_file({"APP_TIMEOUT": 90})
    container = init(test_module, config=(EnvSource(prefix="APP_"), FileSource(config_path, prefix="APP_")))
    config = container.get(AppConfig)
    assert config.TIMEOUT == 120

def test_lazy_component_instantiation():
    container = init(test_module)
    assert "LazyComponent Instantiated!" not in log_capture
    proxy = container.get(LazyComponent)
    first_access_time = proxy.created_at
    assert "LazyComponent Instantiated!" in log_capture
    log_capture.clear()
    second_access_time = proxy.created_at
    assert first_access_time == second_access_time
    assert "LazyComponent Instantiated!" not in log_capture

def test_primary_component_wins():
    container = init(test_module)
    db = container.get(Database)
    assert isinstance(db, PostgresDB)

def test_conditional_component_active_by_profile():
    container_prod = init(test_module, profiles=("prod",))
    cache_client_prod = container_prod.get(CacheClient)
    assert isinstance(cache_client_prod.cache, RedisCache)

def test_on_missing_component_fallback():
    container_dev = init(test_module, profiles=("dev",))
    cache_client_dev = container_dev.get(CacheClient)
    assert isinstance(cache_client_dev.cache, InMemoryCache)

def test_request_scope():
    container = init(test_module, custom_scopes=test_scopes)
    token1 = container.activate_scope("request", "req-1")
    try:
        instance1_req1 = container.get(NeedsScoped)
        instance2_req1 = container.get(NeedsScoped)
        assert instance1_req1 is instance2_req1
        assert instance1_req1.req_data.request_id == "req-1"
        assert f"RequestScopedData created for request: req-1" in log_capture
        assert log_capture.count(f"RequestScopedData created for request: req-1") == 1
    finally:
        container.deactivate_scope("request", token1)
    log_capture.clear()
    token2 = container.activate_scope("request", "req-2")
    try:
        instance1_req2 = container.get(NeedsScoped)
        assert instance1_req2 is not instance1_req1
        assert instance1_req2.req_data.request_id == "req-2"
        assert f"RequestScopedData created for request: req-2" in log_capture
        assert log_capture.count(f"RequestScopedData created for request: req-2") == 1
    finally:
        container.deactivate_scope("request", token2)

def test_qualifier_injection():
    container = init(test_module)
    payment_service = container.get(PaymentService)
    assert len(payment_service.senders) == 2
    sender_types = {type(s) for s in payment_service.senders}
    assert sender_types == {StripeSender, PaypalSender}
    log_capture.clear()
    for sender in payment_service.senders:
        sender.send("Test message")
    assert any("Stripe: Test message" in msg for msg in log_capture)
    assert any("Paypal: Test message" in msg for msg in log_capture)
    assert all("Email:" not in msg for msg in log_capture)

def test_aop_interceptor_applied():
    container = init(test_module)
    audited_service = container.get(AuditedService)
    log_capture.clear()
    result = audited_service.perform_action(123)
    assert result == "Action 123 done"
    assert "AUDIT - Entering: AuditedService.perform_action" in log_capture
    assert "Performing action 123" in log_capture
    assert "AUDIT - Exiting: AuditedService.perform_action" in log_capture

def test_cleanup_called():
    container = init(test_module)
    holder = container.get(ResourceHolder)
    assert not holder.closed
    assert "ResourceHolder created" in log_capture
    log_capture.clear()
    container.cleanup_all()
    assert holder.closed
    assert "ResourceHolder closed" in log_capture
    
    
