/*
 * Copyright (c) 2026, Gregory Bertilson <gregory@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/ByteBuffer.h>
#include <LibCore/EventLoop.h>
#include <LibMedia/IncrementallyPopulatedStream.h>
#include <LibTest/TestCase.h>
#include <LibThreading/Thread.h>

static ByteBuffer make_test_data(size_t size)
{
    auto buffer = MUST(ByteBuffer::create_uninitialized(size));
    for (size_t i = 0; i < size; i++)
        buffer[i] = static_cast<u8>(i);
    return buffer;
}

TEST_CASE(create_empty)
{
    auto stream = Media::IncrementallyPopulatedStream::create_empty();
    EXPECT(!stream->expected_size().has_value());

    stream->set_expected_size(500);
    EXPECT(stream->expected_size().has_value());
    EXPECT_EQ(stream->expected_size().value(), 500u);
}

TEST_CASE(create_from_data_and_buffer)
{
    auto data = make_test_data(256);

    auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(data);
    EXPECT(stream->expected_size().has_value());
    EXPECT_EQ(stream->expected_size().value(), 256u);
    EXPECT_EQ(stream->size(), 256u);
}

TEST_CASE(cursor_seek_modes)
{
    auto data = make_test_data(100);
    auto stream = Media::IncrementallyPopulatedStream::create_from_data(data.bytes());
    auto cursor = stream->create_cursor();

    EXPECT_EQ(cursor->position(), 0u);
    EXPECT_EQ(cursor->size(), 100u);

    MUST(cursor->seek(50, SeekMode::SetPosition));
    EXPECT_EQ(cursor->position(), 50u);

    MUST(cursor->seek(10, SeekMode::FromCurrentPosition));
    EXPECT_EQ(cursor->position(), 60u);

    MUST(cursor->seek(-10, SeekMode::FromEndPosition));
    EXPECT_EQ(cursor->position(), 90u);
}

TEST_CASE(cursor_read_operations)
{
    auto data = make_test_data(100);
    auto stream = Media::IncrementallyPopulatedStream::create_from_data(data.bytes());
    auto cursor = stream->create_cursor();

    Array<u8, 10> buffer;
    auto bytes_read = MUST(cursor->read_into(buffer));
    EXPECT_EQ(bytes_read, 10u);
    EXPECT_EQ(cursor->position(), 10u);
    for (size_t i = 0; i < 10; i++)
        EXPECT_EQ(buffer[i], static_cast<u8>(i));

    MUST(cursor->seek(50, SeekMode::SetPosition));
    MUST(cursor->read_into(buffer));
    for (size_t i = 0; i < 10; i++)
        EXPECT_EQ(buffer[i], static_cast<u8>(50 + i));

    MUST(cursor->seek(95, SeekMode::SetPosition));
    bytes_read = MUST(cursor->read_into(buffer));
    EXPECT_EQ(bytes_read, 5u);
    for (size_t i = 0; i < 5; i++)
        EXPECT_EQ(buffer[i], static_cast<u8>(95 + i));

    MUST(cursor->seek(100, SeekMode::SetPosition));
    auto result = cursor->read_into(buffer);
    EXPECT(result.is_error());
    EXPECT_EQ(result.error().category(), Media::DecoderErrorCategory::EndOfStream);

    MUST(cursor->seek(0, SeekMode::SetPosition));
    bytes_read = MUST(cursor->read_into(buffer.span().trim(0)));
    EXPECT_EQ(bytes_read, 0u);
    EXPECT_EQ(cursor->position(), 0u);
}

TEST_CASE(sequential_reads)
{
    auto data = make_test_data(256);
    auto stream = Media::IncrementallyPopulatedStream::create_from_data(data.bytes());
    auto cursor = stream->create_cursor();

    for (size_t i = 0; i < 256; i += 16) {
        Array<u8, 16> buffer;
        auto bytes_read = MUST(cursor->read_into(buffer));
        EXPECT_EQ(bytes_read, 16u);

        for (size_t j = 0; j < 16; j++)
            EXPECT_EQ(buffer[j], static_cast<u8>(i + j));
    }

    EXPECT_EQ(cursor->position(), 256u);
}

TEST_CASE(multiple_cursors_independent)
{
    auto data = make_test_data(100);
    auto stream = Media::IncrementallyPopulatedStream::create_from_data(data.bytes());
    auto cursor1 = stream->create_cursor();
    auto cursor2 = stream->create_cursor();

    MUST(cursor1->seek(10, SeekMode::SetPosition));
    MUST(cursor2->seek(50, SeekMode::SetPosition));

    EXPECT_EQ(cursor1->position(), 10u);
    EXPECT_EQ(cursor2->position(), 50u);

    Array<u8, 5> buffer1;
    Array<u8, 5> buffer2;
    MUST(cursor1->read_into(buffer1));
    MUST(cursor2->read_into(buffer2));

    for (size_t i = 0; i < 5; i++) {
        EXPECT_EQ(buffer1[i], static_cast<u8>(10 + i));
        EXPECT_EQ(buffer2[i], static_cast<u8>(50 + i));
    }
}

TEST_CASE(add_chunks_incrementally)
{
    auto stream = Media::IncrementallyPopulatedStream::create_empty();

    constexpr size_t data_size = 100;
    auto data = make_test_data(data_size);
    stream->add_chunk_at(0, data.bytes().trim(50));

    stream->add_chunk_at(50, data.bytes().slice(50));
    stream->close();

    EXPECT(stream->expected_size().has_value());
    EXPECT_EQ(stream->expected_size().value(), data_size);

    auto cursor = stream->create_cursor();
    Array<u8, data_size> buffer;
    auto bytes_read = MUST(cursor->read_into(buffer));

    EXPECT_EQ(bytes_read, data_size);
    for (size_t i = 0; i < data_size; i++)
        EXPECT_EQ(buffer[i], static_cast<u8>(i));
}

TEST_CASE(add_overlapping_chunks)
{
    auto stream = Media::IncrementallyPopulatedStream::create_empty();

    constexpr size_t data_size = 100;
    auto data = make_test_data(data_size);
    stream->add_chunk_at(0, data.bytes().trim(50));
    stream->add_chunk_at(40, data.bytes().slice(40));

    auto cursor = stream->create_cursor();
    Array<u8, data_size> buffer;
    auto bytes_read = MUST(cursor->read_into(buffer));

    EXPECT_EQ(bytes_read, data_size);
    for (size_t i = 0; i < data_size; i++)
        EXPECT_EQ(buffer[i], static_cast<u8>(i));
}

TEST_CASE(add_chunk_at_offset)
{
    Core::EventLoop loop;

    auto stream = Media::IncrementallyPopulatedStream::create_empty();
    stream->set_expected_size(100);
    stream->set_data_request_callback([](u64) { });

    auto data = make_test_data(80);
    stream->add_chunk_at(0, data.bytes().trim(30));
    stream->add_chunk_at(50, data.bytes().slice(50));

    auto cursor = stream->create_cursor();
    MUST(cursor->seek(50, SeekMode::SetPosition));

    Array<u8, 30> buffer;
    auto bytes_read = MUST(cursor->read_into(buffer));

    EXPECT_EQ(bytes_read, 30u);
    for (size_t i = 0; i < 30; i++)
        EXPECT_EQ(buffer[i], static_cast<u8>(50 + i));
}

TEST_CASE(cursor_abort_and_reset)
{
    auto stream = Media::IncrementallyPopulatedStream::create_empty();
    stream->set_expected_size(100);

    auto cursor = stream->create_cursor();

    EXPECT(!cursor->is_blocked());

    IGNORE_USE_IN_ESCAPING_LAMBDA Atomic<bool> read_completed { false };
    IGNORE_USE_IN_ESCAPING_LAMBDA Atomic<bool> was_aborted { false };

    auto thread = Threading::Thread::construct("TestAbort"sv, [&, cursor]() -> intptr_t {
        Array<u8, 10> buffer;
        auto result = cursor->read_into(buffer);
        read_completed = true;
        was_aborted = result.is_error() && result.error().category() == Media::DecoderErrorCategory::Aborted;
        return 0;
    });

    thread->start();

    while (!cursor->is_blocked())
        ;
    EXPECT(cursor->is_blocked());

    cursor->abort();
    MUST(thread->join());

    EXPECT_EQ(cursor->is_blocked(), false);
    EXPECT(read_completed.load());
    EXPECT(was_aborted.load());

    // After aborting a read, reset_abort() should allow us to read again.
    cursor->reset_abort();
    auto data = make_test_data(100);
    stream->add_chunk_at(0, data.bytes());

    Array<u8, 10> buffer;
    auto result = cursor->read_into(buffer);
    EXPECT(!result.is_error());
    EXPECT_EQ(result.value(), 10u);
}

TEST_CASE(redundant_chunk_within_existing_chunk_at_nonzero_offset)
{
    // Regression test: add_chunk_at used to compare chunk.size() (a relative byte count)
    // against new_chunk_end (an absolute file offset). When the existing chunk started at
    // a non-zero offset, chunk.size() < new_chunk_end even if the new data was fully
    // covered, causing the buffer to be shrunk and data beyond the new chunk's end to be lost.
    auto stream = Media::IncrementallyPopulatedStream::create_empty();

    constexpr size_t data_size = 200;
    auto data = make_test_data(data_size);

    // Add a chunk at a non-zero offset covering [100, 120).
    stream->add_chunk_at(100, data.bytes().slice(100, 20));

    // Add a redundant chunk fully within [100, 120), specifically [105, 115).
    // With the bug, this shrinks the existing chunk to [100, 115), losing bytes [115, 120).
    stream->add_chunk_at(105, data.bytes().slice(105, 10));

    stream->close();

    auto cursor = stream->create_cursor();
    MUST(cursor->seek(100, SeekMode::SetPosition));

    Array<u8, 20> buffer;
    auto bytes_read = MUST(cursor->read_into(buffer));

    EXPECT_EQ(bytes_read, 20u);
    for (size_t i = 0; i < 20; i++)
        EXPECT_EQ(buffer[i], static_cast<u8>(100 + i));
}

TEST_CASE(data_request_callback_invoked)
{
    Core::EventLoop loop;

    // Stream size must be larger than FORWARD_REQUEST_THRESHOLD (1 MiB) to test callback
    static constexpr u64 stream_size = 2 * MiB;
    static constexpr u64 initial_chunk_size = 100;
    static constexpr u64 seek_position = stream_size - 100;

    auto stream = Media::IncrementallyPopulatedStream::create_empty();
    stream->set_expected_size(stream_size);

    // Add initial chunk so the callback logic can be triggered
    auto initial_data = make_test_data(initial_chunk_size);
    stream->add_chunk_at(0, initial_data.bytes());

    bool callback_invoked { false };
    u64 requested_offset { 0 };

    stream->set_data_request_callback([&](u64 offset) {
        auto data = make_test_data(100);
        stream->add_chunk_at(seek_position, data.bytes());
        callback_invoked = true;
        requested_offset = offset;
    });

    auto cursor = stream->create_cursor();
    MUST(cursor->seek(seek_position, SeekMode::SetPosition));

    auto thread = Threading::Thread::construct("TestCallback"sv, [cursor]() -> intptr_t {
        Array<u8, 10> buffer;
        MUST(cursor->read_into(buffer));
        return 0;
    });
    thread->start();

    auto start_time = MonotonicTime::now_coarse();
    while (!callback_invoked) {
        loop.pump(Core::EventLoop::WaitMode::PollForEvents);
        if (MonotonicTime::now_coarse() - start_time > AK::Duration::from_seconds(1))
            break;
    }

    EXPECT(callback_invoked);
    EXPECT(requested_offset >= initial_chunk_size);

    MUST(thread->join());
}
