import pytest

from mirage.commands.builtin.generic.awk import awk
from mirage.commands.errors import UsageError
from mirage.types import PathSpec


def _spec(path: str) -> PathSpec:
    return PathSpec(resource_path=(path).strip("/"),
                    virtual=path,
                    directory=path,
                    resolved=True)


def _make_backend(files: dict[str, bytes]):

    async def read_bytes(path):
        key = path.virtual if isinstance(path, PathSpec) else path
        if key not in files:
            raise FileNotFoundError(key)
        return files[key]

    async def read_stream(path):
        assert isinstance(path, PathSpec)
        key = path.virtual
        if key not in files:
            raise FileNotFoundError(key)
        yield files[key]

    return read_bytes, read_stream


async def _drain(stdout) -> bytes:
    if stdout is None:
        return b""
    if isinstance(stdout, bytes):
        return stdout
    return b"".join([c async for c in stdout])


@pytest.mark.asyncio
async def test_awk_stdin_print_field():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print $1}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"alpha beta\ngamma delta\n",
    )
    assert (await _drain(output)).decode() == "alpha\ngamma\n"


@pytest.mark.asyncio
async def test_awk_field_separator():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print $2}", ),
        {"F": ","},
        read_bytes=rb,
        read_stream=rs,
        stdin=b"a,b,c\nd,e,f\n",
    )
    assert (await _drain(output)).decode() == "b\ne\n"


@pytest.mark.asyncio
async def test_awk_variable_assignment():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print x}", ),
        {"v": ["x=hello"]},
        read_bytes=rb,
        read_stream=rs,
        stdin=b"line\n",
    )
    assert (await _drain(output)).decode() == "hello\n"


@pytest.mark.asyncio
async def test_awk_numeric_comparison():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("$1 > 2 {print $1}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"1\n2\n3\n4\n",
    )
    assert (await _drain(output)).decode() == "3\n4\n"


@pytest.mark.asyncio
async def test_awk_regex_condition():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("/foo/ {print $0}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"foo bar\nbaz\nfoobar\n",
    )
    assert (await _drain(output)).decode() == "foo bar\nfoobar\n"


@pytest.mark.asyncio
async def test_awk_end_block_accumulator():
    """sum += $1 with END {print sum} should emit total."""
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{sum += $1} END {print sum}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"10\n20\n30\n",
    )
    assert (await _drain(output)).decode() == "60\n"


@pytest.mark.asyncio
async def test_awk_reads_from_file():
    rb, rs = _make_backend({"/data.txt": b"hello world\n"})
    output, io = await awk(
        [_spec("/data.txt")],
        ("{print $2}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
    )
    assert (await _drain(output)).decode() == "world\n"
    assert io.cache == ["/data.txt"]


@pytest.mark.asyncio
async def test_awk_program_file_overrides_inline():
    rb, rs = _make_backend({
        "/prog.awk": b"{print $1}\n",
        "/data.txt": b"alpha beta\n",
    })
    output, _ = await awk(
        [_spec("/data.txt")],
        (),
        {"f": _spec("/prog.awk")},
        read_bytes=rb,
        read_stream=rs,
    )
    assert (await _drain(output)).decode() == "alpha\n"


@pytest.mark.asyncio
async def test_awk_missing_program_raises_usage_error():
    rb, rs = _make_backend({})
    with pytest.raises(UsageError, match="usage"):
        await awk([], (), None, read_bytes=rb, read_stream=rs)


@pytest.mark.asyncio
async def test_awk_default_fs_collapses_whitespace():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print $2}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"a   b\n\tx\t \ty\n",
    )
    assert (await _drain(output)).decode() == "b\ny\n"


@pytest.mark.asyncio
async def test_awk_explicit_single_space_fs_collapses_whitespace():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print $2}", ),
        {"F": " "},
        read_bytes=rb,
        read_stream=rs,
        stdin=b"a   b\n",
    )
    assert (await _drain(output)).decode() == "b\n"


@pytest.mark.asyncio
async def test_awk_empty_fs_splits_characters():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print $2}", ),
        {"F": ""},
        read_bytes=rb,
        read_stream=rs,
        stdin=b"abc\n",
    )
    assert (await _drain(output)).decode() == "b\n"


@pytest.mark.asyncio
async def test_awk_processes_all_files_with_continuous_nr():
    rb, rs = _make_backend({
        "/a.txt": b"one\ntwo\n",
        "/b.txt": b"three\n",
    })
    output, io = await awk(
        [_spec("/a.txt"), _spec("/b.txt")],
        ("{print NR, $1}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
    )
    assert (await _drain(output)).decode() == "1 one\n2 two\n3 three\n"
    assert io.cache == ["/a.txt", "/b.txt"]


@pytest.mark.asyncio
async def test_awk_multifile_no_trailing_newline_keeps_lines_separate():
    rb, rs = _make_backend({
        "/a.txt": b"one",
        "/b.txt": b"two\n",
    })
    output, _ = await awk(
        [_spec("/a.txt"), _spec("/b.txt")],
        ("{print NR, $1}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
    )
    assert (await _drain(output)).decode() == "1 one\n2 two\n"


@pytest.mark.asyncio
async def test_awk_repeated_v_assignments():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print a, b}", ),
        {"v": ["a=1", "b=2"]},
        read_bytes=rb,
        read_stream=rs,
        stdin=b"line\n",
    )
    assert (await _drain(output)).decode() == "1 2\n"


@pytest.mark.asyncio
async def test_awk_v_value_containing_equals():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print x}", ),
        {"v": ["x=a=b"]},
        read_bytes=rb,
        read_stream=rs,
        stdin=b"line\n",
    )
    assert (await _drain(output)).decode() == "a=b\n"


@pytest.mark.asyncio
async def test_awk_print_empty_string_emits_blank_line():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ('{print ""}', ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"one\ntwo\n",
    )
    assert (await _drain(output)).decode() == "\n\n"


@pytest.mark.asyncio
async def test_awk_action_without_print_emits_nothing():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{x += 1}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"one\ntwo\n",
    )
    assert (await _drain(output)).decode() == ""


@pytest.mark.asyncio
async def test_awk_brace_literal_in_print():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ('{print "}"}', ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"line\n",
    )
    assert (await _drain(output)).decode() == "}\n"


@pytest.mark.asyncio
async def test_awk_accumulator_non_numeric_coerces():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{sum += $1} END {print sum}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"3\nabc\n2.5x\n",
    )
    assert (await _drain(output)).decode() == "5.5\n"


@pytest.mark.asyncio
async def test_awk_program_file_missing_raises_usage_error():
    rb, rs = _make_backend({"/data.txt": b"x\n"})
    with pytest.raises(UsageError, match="No such file"):
        await awk(
            [_spec("/data.txt")],
            (),
            {"f": _spec("/missing.awk")},
            read_bytes=rb,
            read_stream=rs,
        )


@pytest.mark.asyncio
async def test_awk_program_file_with_multiple_data_files():
    rb, rs = _make_backend({
        "/prog.awk": b"{print NR, $1}\n",
        "/a.txt": b"one\n",
        "/b.txt": b"two\n",
    })
    output, io = await awk(
        [_spec("/a.txt"), _spec("/b.txt")],
        (),
        {"f": _spec("/prog.awk")},
        read_bytes=rb,
        read_stream=rs,
    )
    assert (await _drain(output)).decode() == "1 one\n2 two\n"
    assert io.cache == ["/a.txt", "/b.txt"]


@pytest.mark.asyncio
async def test_awk_begin_end_resolve_v_variables():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("BEGIN {print x} END {print x}", ),
        {"v": ["x=hi"]},
        read_bytes=rb,
        read_stream=rs,
        stdin=b"line\n",
    )
    assert (await _drain(output)).decode() == "hi\nhi\n"


@pytest.mark.asyncio
async def test_awk_duplicate_v_last_wins():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print x}", ),
        {"v": ["x=first", "x=second"]},
        read_bytes=rb,
        read_stream=rs,
        stdin=b"line\n",
    )
    assert (await _drain(output)).decode() == "second\n"


@pytest.mark.asyncio
async def test_awk_begin_bare_print_emits_blank_line():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ('BEGIN {print} {print $1}', ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"a\n",
    )
    assert (await _drain(output)).decode() == "\na\n"


@pytest.mark.asyncio
async def test_awk_brace_literal_with_condition():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ('/x/ {print "}"}', ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"x\ny\n",
    )
    assert (await _drain(output)).decode() == "}\n"


@pytest.mark.asyncio
async def test_awk_repeated_program_files_concatenate():
    rb, rs = _make_backend({
        "/p1.awk": b"{sum += $1}\n",
        "/p2.awk": b"END {print sum}\n",
        "/nums.txt": b"1\n2\n3\n",
    })
    output, _ = await awk(
        [_spec("/nums.txt")],
        (),
        {"f": [_spec("/p1.awk"), _spec("/p2.awk")]},
        read_bytes=rb,
        read_stream=rs,
    )
    assert (await _drain(output)).decode() == "6\n"


@pytest.mark.asyncio
async def test_awk_simple_assignment_executes():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{x = 1; print x}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"line\n",
    )
    assert (await _drain(output)).decode() == "1\n"


@pytest.mark.asyncio
async def test_awk_assignment_from_field():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{x = $2; print x}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"a b\n",
    )
    assert (await _drain(output)).decode() == "b\n"


@pytest.mark.asyncio
async def test_awk_ofs_joins_print_arguments():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ('BEGIN{OFS=":"} {print $1, $2}', ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"name age\nalice 30\n",
    )
    assert (await _drain(output)).decode() == "name:age\nalice:30\n"


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "program, expected",
    [
        ("{{print $1}}", "Welcome\nInstall\n"),
        ("{{{print $1}}}", "Welcome\nInstall\n"),
        ("{{print $1}; print $2}", "Welcome\nto\nInstall\nit\n"),
        ("{print $1;{print $2}}", "Welcome\nto\nInstall\nit\n"),
    ],
)
async def test_awk_compound_statement_runs_its_body(program, expected):
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        (program, ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"Welcome to x\nInstall it\n",
    )
    assert (await _drain(output)).decode() == expected


@pytest.mark.asyncio
async def test_awk_semicolon_inside_string_is_not_a_separator():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ('{print "a;b", $1}', ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"x\n",
    )
    assert (await _drain(output)).decode() == "a;b x\n"


@pytest.mark.asyncio
async def test_awk_rejects_arithmetic_assignment():
    rb, rs = _make_backend({})
    with pytest.raises(UsageError, match="unsupported construct"):
        await awk(
            [],
            ("{x = y + 1; print x}", ),
            None,
            read_bytes=rb,
            read_stream=rs,
            stdin=b"line\n",
        )


@pytest.mark.asyncio
async def test_awk_rejects_function_call_in_print():
    rb, rs = _make_backend({})
    with pytest.raises(UsageError, match=r"unsupported construct.*toupper"):
        await awk(
            [],
            ("{print toupper($1)}", ),
            None,
            read_bytes=rb,
            read_stream=rs,
            stdin=b"line\n",
        )


@pytest.mark.asyncio
async def test_awk_rejects_printf():
    rb, rs = _make_backend({})
    with pytest.raises(UsageError, match="unsupported construct"):
        await awk(
            [],
            ('{printf "%s\\n", $1}', ),
            None,
            read_bytes=rb,
            read_stream=rs,
            stdin=b"line\n",
        )


@pytest.mark.asyncio
async def test_awk_rejects_if_statement():
    rb, rs = _make_backend({})
    with pytest.raises(UsageError, match="unsupported construct"):
        await awk(
            [],
            ("{if ($1) print $1}", ),
            None,
            read_bytes=rb,
            read_stream=rs,
            stdin=b"line\n",
        )


@pytest.mark.asyncio
async def test_awk_rejects_tilde_match_condition():
    rb, rs = _make_backend({})
    with pytest.raises(UsageError, match="unsupported construct"):
        await awk(
            [],
            ("$1 ~ /x/ {print}", ),
            None,
            read_bytes=rb,
            read_stream=rs,
            stdin=b"x\n",
        )


@pytest.mark.asyncio
async def test_awk_rejects_arithmetic_in_condition():
    rb, rs = _make_backend({})
    with pytest.raises(UsageError, match="unsupported construct"):
        await awk(
            [],
            ("NR % 2 == 0 {print}", ),
            None,
            read_bytes=rb,
            read_stream=rs,
            stdin=b"a\nb\n",
        )


@pytest.mark.asyncio
async def test_awk_rejects_program_file_with_unsupported_statement():
    rb, rs = _make_backend({"/p.awk": b'{gsub(/a/, "b"); print}\n'})
    with pytest.raises(UsageError, match="unsupported construct"):
        await awk(
            [],
            (),
            {"f": [_spec("/p.awk")]},
            read_bytes=rb,
            read_stream=rs,
            stdin=b"line\n",
        )


@pytest.mark.asyncio
async def test_awk_unset_variable_prints_empty():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print foo}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"line\n",
    )
    assert (await _drain(output)).decode() == "\n"


@pytest.mark.asyncio
async def test_awk_out_of_range_field_prints_empty():
    rb, rs = _make_backend({})
    output, _ = await awk(
        [],
        ("{print $5}", ),
        None,
        read_bytes=rb,
        read_stream=rs,
        stdin=b"one two\n",
    )
    assert (await _drain(output)).decode() == "\n"
