import threading


class FakeClock:
    def __init__(self):
        self.now = 0.0

    def __call__(self):
        return self.now

    def advance(self, seconds):
        self.now += seconds


class FakeEngine:
    def __init__(self, name):
        self.name = name
        self.disposed = False

    def dispose(self):
        self.disposed = True


def settings(**overrides):
    values = {
        "idle_ttl": 900,
        "max_idle_pools": 20,
        "drain_timeout": 30,
    }
    values.update(overrides)
    return values


def test_registry_reuses_same_engine_and_invalidates_new_version():
    from app.core.data_source.models import PoolKey
    from app.core.data_source.pool_registry import PoolRegistry

    clock = FakeClock()
    registry = PoolRegistry(settings(), clock=clock)
    first = PoolKey("source-1", 1, "hash-a")
    second = PoolKey("source-1", 2, "hash-b")
    created = []

    def factory(key):
        engine = FakeEngine(f"{key.credential_version}-{len(created)}")
        created.append(engine)
        return engine

    with registry.lease(first, lambda: factory(first)) as engine_a:
        with registry.lease(first, lambda: factory(first)) as engine_b:
            assert engine_a is engine_b

    with registry.lease(second, lambda: factory(second)) as engine_c:
        assert engine_c is not engine_a

    assert len(created) == 2
    assert engine_a.disposed is True
    assert registry.snapshot()[0].credential_version == 2


def test_idle_pool_is_evicted_only_after_last_lease_returns():
    from app.core.data_source.models import PoolKey
    from app.core.data_source.pool_registry import PoolRegistry

    clock = FakeClock()
    registry = PoolRegistry(settings(), clock=clock)
    key = PoolKey("source-1", 1, "hash-a")
    engine = FakeEngine("one")

    with registry.lease(key, lambda: engine):
        clock.advance(901)
        assert registry.evict_idle() == 0
        assert engine.disposed is False

    assert registry.evict_idle() == 0
    clock.advance(901)
    assert registry.evict_idle() == 1
    assert engine.disposed is True


def test_invalidation_drains_existing_lease_and_allows_rebuild():
    from app.core.data_source.models import PoolKey
    from app.core.data_source.pool_registry import PoolRegistry

    registry = PoolRegistry(settings())
    key = PoolKey("source-1", 1, "hash-a")
    first = FakeEngine("first")
    second = FakeEngine("second")

    with registry.lease(key, lambda: first):
        registry.invalidate("source-1", reason="configuration_changed")
        assert first.disposed is False
        with registry.lease(key, lambda: second) as rebuilt:
            assert rebuilt is second

    assert first.disposed is True
    assert second.disposed is False


def test_lru_bounds_idle_pool_count():
    from app.core.data_source.models import PoolKey
    from app.core.data_source.pool_registry import PoolRegistry

    clock = FakeClock()
    registry = PoolRegistry(
        settings(max_idle_pools=2, idle_ttl=9999),
        clock=clock,
    )
    engines = []
    for index in range(3):
        key = PoolKey(f"source-{index}", 1, f"hash-{index}")
        engine = FakeEngine(str(index))
        engines.append(engine)
        with registry.lease(key, lambda engine=engine: engine):
            pass
        clock.advance(1)

    assert registry.evict_idle() == 1
    assert engines[0].disposed is True
    assert len(registry.snapshot()) == 2


def test_concurrent_leases_create_one_engine_and_return_all_leases():
    from app.core.data_source.models import PoolKey
    from app.core.data_source.pool_registry import PoolRegistry

    registry = PoolRegistry(settings())
    key = PoolKey("source-1", 1, "hash-a")
    created = []
    barrier = threading.Barrier(50)
    errors = []

    def factory():
        engine = FakeEngine(f"engine-{len(created)}")
        created.append(engine)
        return engine

    def worker():
        try:
            barrier.wait(timeout=5)
            with registry.lease(key, factory):
                pass
        except Exception as exc:  # pragma: no cover - asserted below
            errors.append(exc)

    threads = [threading.Thread(target=worker) for _ in range(50)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join(timeout=5)

    assert errors == []
    assert len(created) == 1
    status = registry.snapshot()[0]
    assert status.leases == 0


def test_draining_pool_is_force_disposed_after_grace_period():
    from app.core.data_source.models import PoolKey
    from app.core.data_source.pool_registry import PoolRegistry

    clock = FakeClock()
    registry = PoolRegistry(settings(drain_timeout=30), clock=clock)
    key = PoolKey("source-1", 1, "hash-a")
    engine = FakeEngine("one")

    with registry.lease(key, lambda: engine):
        registry.invalidate("source-1", reason="datasource_deleted")
        clock.advance(31)
        assert registry.expire_draining() == 1
        assert engine.disposed is True


def test_registry_injects_its_clock_into_each_circuit_breaker():
    from app.core.data_source.models import PoolKey
    from app.core.data_source.pool_registry import PoolRegistry

    clock = FakeClock()
    registry = PoolRegistry(settings(), clock=clock)
    key = PoolKey("source-1", 1, "hash-a")
    with registry.lease(key, lambda: FakeEngine("one")):
        pass

    breaker = registry.breaker_for(key)
    breaker.record_failure()
    breaker.record_failure()
    breaker.record_failure()
    clock.advance(30)

    with breaker.probe_permission():
        breaker.record_success()

    assert breaker.state == "closed"
