import asyncio
import random
from uuid import UUID

import pytest

from ldp.graph import (
    CallID,
    compute_graph,
    eval_mode,
    get_call_id,
    get_run_id,
    get_training_mode,
    op_call,
    set_training_mode,
    train_mode,
)
from ldp.graph.op_utils import _RUN_ID


@pytest.mark.asyncio
async def test_run_ids():
    # check that we don't create a new run ID if we're
    # already in a context
    async with compute_graph():
        run_id_1 = get_run_id()
        async with compute_graph():
            run_id_2 = get_run_id()
            assert (
                run_id_1 == run_id_2
            ), "Should not create a new run ID if already in a run context."

    # Check that after exiting the context, _RUN_ID is no longer set
    with pytest.raises(LookupError):
        _RUN_ID.get()

    # Now check that we don't clobber in coroutines
    async def run_id_test() -> UUID:
        async with compute_graph():
            # Wait randomly, giving time for other calls to potentially clobber
            # this one.
            await asyncio.sleep(random.uniform(0.1, 0.5))
            return get_run_id()

    run_ids = await asyncio.gather(*[run_id_test() for _ in range(10)])
    assert len(run_ids) == len(
        set(run_ids)
    ), "At least two compute graphs had the same run ID, which indicates a clobber."


@pytest.mark.asyncio
async def test_call_ids():
    # check that we don't create a new call ID if we're
    # already in a context
    async with compute_graph(), op_call():
        call_id_1 = get_call_id()
        async with op_call():
            call_id_2 = get_call_id()
            assert (
                call_id_1 == call_id_2
            ), "Should not create a new call ID if already in a call context."

    # Check we cannot create a call ID if not in a run context
    with pytest.raises(RuntimeError, match=r".*not inside compute graph context.*"):
        async with op_call():
            pass

    async def call_test() -> CallID:
        async with op_call():
            # Wait randomly, giving time for other calls to potentially clobber
            # this one.
            await asyncio.sleep(random.uniform(0.1, 0.5))
            return get_call_id()

    async with compute_graph():
        call_ids = await asyncio.gather(*[call_test() for _ in range(10)])
    assert len(call_ids) == len(
        set(call_ids)
    ), "At least two compute graphs had the same run ID, which indicates a clobber."


@pytest.mark.asyncio
async def test_training_mode():
    with eval_mode():
        assert not get_training_mode(), "Training mode was not set to False"
    assert (
        get_training_mode()
    ), "Training mode should have been reset to True after exiting context"

    set_training_mode(False)
    with train_mode():
        assert get_training_mode(), "Training mode was not set to True"
    assert (
        not get_training_mode()
    ), "Training mode should have been reset to False after exiting context"

    # Put back to training for next round of tests
    set_training_mode(True)

    async def training_mode_test(i: int):
        train = i % 2 == 0
        set_training_mode(train)
        # wait a random amount of time to give a chance for other coroutines to clobber
        await asyncio.sleep(random.uniform(0.1, 0.5))

        assert get_training_mode() == train, "Training mode was overwritten."

    await asyncio.gather(*[training_mode_test(i) for i in range(10)])
    # Make sure training_mode_test didn't change our training mode
    assert get_training_mode()

    # Make sure nesting works
    with train_mode():
        assert get_training_mode()
        with eval_mode():
            assert not get_training_mode()
            with train_mode():
                assert get_training_mode()
            assert not get_training_mode()
        assert get_training_mode()

    # Make sure modes are set correctly in coroutines

    @train_mode()
    async def train_then_eval():
        assert get_training_mode()
        await asyncio.sleep(0.0)
        async with eval_mode():
            assert not get_training_mode()

    @eval_mode()
    async def eval_then_train():
        assert not get_training_mode()
        await asyncio.sleep(0.0)
        async with train_mode():
            assert get_training_mode()

    await asyncio.gather(train_then_eval(), eval_then_train())
