#include "async.h"

#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <array>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <mutex>
#include <semaphore>
#include <thread>

#include "trace.h"

using namespace ::testing;

using async::AwaitableType;
using namespace std::literals::chrono_literals;

int uptime() {
    static std::chrono::system_clock::time_point start =
        std::chrono::system_clock::now();

    return std::chrono::duration_cast<std::chrono::microseconds>(
               std::chrono::system_clock::now() - start)
        .count();
}

template <typename... Args>
void log(int i, const char* fmt, Args... args) {
    if (i != 3) {
        return;
    }
    printf("[%10i] ", uptime());
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-security"
    printf(fmt, args...);
#pragma clang diagnostic pop
    printf("\n");
}

namespace tracing {
void trace(TraceEvent) {}
}  // namespace tracing

struct SimpleAsyncTest : public Test {
    SimpleAsyncTest() : done(0) {}

    std::binary_semaphore done;

    async::task<> test_task() {
        co_await async::await(AwaitableType::kUartRx);
        done.release();
    }

    void SetUp() { terminate = false; }

    static std::atomic<bool> terminate;
};

std::atomic<bool> SimpleAsyncTest::terminate;

TEST_F(SimpleAsyncTest, EnqueueResume) {
    async::schedule(test_task().h);
    std::thread t(
        []() { async::main_loop([]() -> bool { return terminate; }); });

    async::resume(AwaitableType::kUartRx);
    EXPECT_TRUE(done.try_acquire_for(1s));
    terminate = true;

    t.join();
}

struct LoopAsyncTest : public Test {
    int in_isr = 0;
    std::mutex m;
    std::counting_semaphore<123456> processed;

    LoopAsyncTest() : processed(0) {}

    async::task<> loop_task() {
        while (!terminate) {
            co_await async::await(AwaitableType::kUartRx);
            {
                std::scoped_lock lock(m);
                while (in_isr > 0) {
                    processed.release();
                    log(1, "processed.release() in_isr: %i", in_isr);
                    in_isr--;
                }
            }
        }
    }

    void SetUp() { terminate = false; }

    static std::atomic<bool> terminate;
};
std::atomic<bool> LoopAsyncTest::terminate;

TEST_F(LoopAsyncTest, ManyEnqueueResume) {
    constexpr int kLoops = 100;

    async::schedule(loop_task().h);
    std::thread t(
        []() { async::main_loop([]() -> bool { return terminate; }); });

    for (int i = 0; i < kLoops; i++) {
        std::scoped_lock lock(m);
        in_isr++;
        async::resume(AwaitableType::kUartRx);
        log(1, "async::resume() in_isr: %i", in_isr);
    }
    int done = 0;
    for (int i = 0; i < kLoops; i++) {
        if (processed.try_acquire_for(2s)) {
            {
                std::scoped_lock lock(m);
                log(1, "processed acquired");
            }
            done++;
        } else {
            log(1, "skipping %i", i);
            break;
        }
    }

    EXPECT_EQ(done, kLoops);
    log(3, "got all of em, done");

    terminate = true;
    t.join();
}

struct NestedAsync : SimpleAsyncTest {
    async::task<int> interior() { co_return 42; }
    async::task<int> interior_yield() {
        co_await async::delay(0);
        co_return 42;
    }

    async::task<int> generator() {
        int i = 41;
        while (true) {
            co_yield i++;
        }
    }

    std::atomic<bool> voiddone = false;
    std::atomic<int> result = 0;

    async::task<> voidstuff() {
        voiddone = true;
        log(3, "voidstuff()");
        co_return;
    }

    async::task<> nested() {
        log(3, "nested()");
        result = co_await interior();
        log(3, "got result");
        co_await voidstuff();
        log(3, "voidstuff returned");
        done.release();
    }

    async::task<> othernested() {
        log(3, "othernested()");
        result = co_await interior_yield();
        log(3, "got result");
        co_await voidstuff();
        log(3, "voidstuff returned");
        done.release();
    }

    async::task<> call_generator() {
        log(4, "call_generator()");
        auto g = generator();
        log(4, "generator()");

        int a0 = co_await g;
        log(4, "a0: %d", a0);

        int a1 = co_await g;
        log(4, "a1: %d", a1);

        result = a1;

        g.h.destroy();

        done.release();
    }

    static std::binary_semaphore next;
    static std::binary_semaphore ready;
};

std::binary_semaphore NestedAsync::next(0);
std::binary_semaphore NestedAsync::ready(0);

TEST_F(NestedAsync, SimpleNestedTest) {
    while (next.try_acquire());
    while (ready.try_acquire());

    async::schedule(nested().h);
    std::thread t([]() {
        async::main_loop([]() -> bool {
            // for some reason the bare `acquire` fails sometimes :/
            while (!next.try_acquire_for(10ms));
            return terminate;
        });
    });

    ASSERT_FALSE(voiddone);
    ASSERT_EQ(result, 0);

    next.release();

    EXPECT_TRUE(done.try_acquire_for(1s));

    ASSERT_TRUE(voiddone);
    ASSERT_EQ(result, 42);

    terminate = true;
    next.release();

    t.join();
}

TEST_F(NestedAsync, LessSimpleNestedTest) {
    async::schedule(othernested().h);

    ASSERT_FALSE(voiddone);
    ASSERT_EQ(result, 0);

    log(3, "step");
    async::step();  // should proceed until yield

    ASSERT_FALSE(voiddone);
    ASSERT_EQ(result, 0);

    log(3, "step");
    async::step();  // should proceed until end

    EXPECT_EQ(voiddone, true);
    ASSERT_EQ(result, 42);

    EXPECT_TRUE(done.try_acquire_for(1s));
}

TEST_F(NestedAsync, GeneratorTest) {
    async::schedule(call_generator().h);
    std::thread t([]() {
        async::main_loop([]() -> bool {
            ready.release();
            log(3, "loop ready");
            next.acquire();
            return terminate;
        });
    });

    ASSERT_EQ(result, 0);

    log(4, "starting stuff");

    ASSERT_TRUE(ready.try_acquire_for(1s));
    next.release();
    ASSERT_TRUE(ready.try_acquire_for(1s));

    ASSERT_EQ(result, 42);

    EXPECT_TRUE(done.try_acquire_for(1s));

    terminate = true;
    next.release();

    t.join();
}
