# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

import asyncio
from collections.abc import AsyncIterator

import pytest

from mirage.commands.builtin.utils.limit import (CommandTimeoutError,
                                                 apply_limit,
                                                 maybe_with_timeout,
                                                 run_with_timeout)
from mirage.io.types import materialize
from mirage.types import Limit, OnExceed

_TEN = b"".join(f"line{i}\n".encode() for i in range(10))


async def _stream(data: bytes) -> AsyncIterator[bytes]:
    for i in range(0, len(data), 7):
        yield data[i:i + 7]


async def _slow_stream() -> AsyncIterator[bytes]:
    await asyncio.sleep(5)
    yield b"x"


async def _const(value):
    return value


async def _sleep_forever():
    await asyncio.sleep(5)


@pytest.mark.asyncio
async def test_no_limit_passthrough():
    out, io = await apply_limit(_TEN, None)
    assert out == _TEN and io.exit_code == 0 and io.stderr is None


@pytest.mark.asyncio
async def test_under_limit_not_truncated():
    sg = Limit(max_lines=100)
    out, io = await apply_limit(_TEN, sg)
    assert out == _TEN and io.stderr is None


@pytest.mark.asyncio
async def test_truncate_by_lines():
    sg = Limit(max_lines=3)
    out, io = await apply_limit(_TEN, sg)
    assert out == b"line0\nline1\nline2\n"
    assert io.exit_code == 0
    assert b"truncated" in (await materialize(io.stderr))


@pytest.mark.asyncio
async def test_error_by_lines():
    sg = Limit(max_lines=3, on_exceed=OnExceed.ERROR)
    out, io = await apply_limit(_TEN, sg)
    assert out is None
    assert io.exit_code == 1
    assert b"truncated" in (await materialize(io.stderr))


@pytest.mark.asyncio
async def test_truncate_by_bytes():
    sg = Limit(max_bytes=10)
    out, io = await apply_limit(_TEN, sg)
    assert out == _TEN[:10]
    assert b"truncated" in (await materialize(io.stderr))


@pytest.mark.asyncio
async def test_streaming_input_truncates_and_stops_early():
    sg = Limit(max_lines=2)
    out, io = await apply_limit(_stream(_TEN), sg)
    assert out == b"line0\nline1\n"
    assert b"truncated" in (await materialize(io.stderr))


def test_maybe_with_timeout_passthrough_when_no_limit():
    stream = _stream(_TEN)
    assert maybe_with_timeout(stream, None, "cat") is stream


def test_maybe_with_timeout_passthrough_when_bytes():
    assert maybe_with_timeout(_TEN, Limit(timeout_seconds=1), "cat") == _TEN


def test_maybe_with_timeout_passthrough_when_no_timeout():
    stream = _stream(_TEN)
    assert maybe_with_timeout(stream, Limit(max_lines=3), "cat") is stream


def test_maybe_with_timeout_passthrough_when_nonpositive():
    stream = _stream(_TEN)
    assert maybe_with_timeout(stream, Limit(timeout_seconds=0),
                              "cat") is stream


@pytest.mark.asyncio
async def test_maybe_with_timeout_wraps_and_fires():
    wrapped = maybe_with_timeout(_slow_stream(), Limit(timeout_seconds=0.1),
                                 "cat")
    with pytest.raises(CommandTimeoutError):
        await materialize(wrapped)


@pytest.mark.asyncio
async def test_run_with_timeout_returns_result_when_under_budget():
    assert await run_with_timeout(_const(42), 1.0, "sleep") == 42


@pytest.mark.asyncio
async def test_run_with_timeout_no_timeout_when_seconds_falsy():
    assert await run_with_timeout(_const(7), None, "sleep") == 7


@pytest.mark.asyncio
async def test_run_with_timeout_raises_on_overrun():
    with pytest.raises(CommandTimeoutError):
        await run_with_timeout(_sleep_forever(), 0.1, "sleep")
