#include <gtest/gtest.h>
#include <MG_State/EGLState/Core.h>
#include <future>
#include <memory>
#include <thread>

namespace {
    using StateContext = MobileGL::MG_State::EGLState::EGLContext;

    struct EGLFixture {
        StateContext State;
        EGLDisplay Display = EGL_NO_DISPLAY;
        EGLConfig Config = nullptr;
        EGLSurface Surface = EGL_NO_SURFACE;
        StateContext::EGLContextHandle Context = EGL_NO_CONTEXT;
    };

    std::unique_ptr<EGLFixture> CreateFixture() {
        auto fixture = std::make_unique<EGLFixture>();
        fixture->Display = fixture->State.GetDisplay(EGL_DEFAULT_DISPLAY);
        EXPECT_NE(fixture->Display, EGL_NO_DISPLAY);
        EXPECT_TRUE(fixture->State.InitializeDisplay(fixture->Display, nullptr, nullptr));

        EGLint configCount = 0;
        EXPECT_TRUE(fixture->State.ChooseConfig(fixture->Display, nullptr, &fixture->Config, 1, &configCount));
        EXPECT_GE(configCount, 1);
        EXPECT_NE(fixture->Config, nullptr);

        const EGLint surfaceAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
        fixture->Surface = fixture->State.CreatePbufferSurface(fixture->Display, fixture->Config, surfaceAttribs);
        EXPECT_NE(fixture->Surface, EGL_NO_SURFACE);

        fixture->Context = fixture->State.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, nullptr);
        EXPECT_NE(fixture->Context, EGL_NO_CONTEXT);
        return fixture;
    }
}

TEST(EGLStateMakeCurrent, ContextCannotBeCurrentOnTwoThreadsAtOnce) {
    auto fixture = CreateFixture();

    std::promise<void> threadAReady;
    std::promise<void> threadBAttempted;
    std::promise<void> threadAReleased;

    auto threadAReadyFuture = threadAReady.get_future();
    auto threadBAttemptedFuture = threadBAttempted.get_future();
    auto threadAReleasedFuture = threadAReleased.get_future();

    bool threadAAttachOk = false;
    bool threadAReleaseOk = false;
    bool threadBDenied = false;
    EGLint threadBDeniedError = EGL_SUCCESS;
    bool threadBAttachAfterReleaseOk = false;
    EGLint threadBAttachAfterReleaseError = EGL_SUCCESS;

    std::thread threadA([&] {
        threadAAttachOk = fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface,
                                                      fixture->Context);
        threadAReady.set_value();
        threadBAttemptedFuture.wait();
        threadAReleaseOk =
            fixture->State.MakeCurrent(fixture->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        threadAReleased.set_value();
    });

    std::thread threadB([&] {
        threadAReadyFuture.wait();
        threadBDenied = fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface,
                                                   fixture->Context);
        threadBDeniedError = fixture->State.ConsumeError();
        threadBAttempted.set_value();
        threadAReleasedFuture.wait();
        threadBAttachAfterReleaseOk =
            fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context);
        threadBAttachAfterReleaseError = fixture->State.ConsumeError();
        if (threadBAttachAfterReleaseOk) {
            (void)fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        }
    });

    threadA.join();
    threadB.join();

    EXPECT_TRUE(threadAAttachOk);
    EXPECT_TRUE(threadAReleaseOk);
    EXPECT_FALSE(threadBDenied);
    EXPECT_EQ(threadBDeniedError, EGL_BAD_ACCESS);
    EXPECT_TRUE(threadBAttachAfterReleaseOk);
    EXPECT_EQ(threadBAttachAfterReleaseError, EGL_SUCCESS);
}

TEST(EGLStateMakeCurrent, SameThreadRepeatedAttachReleaseDoesNotLeaveStaleOwner) {
    auto fixture = CreateFixture();

    EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context));
    EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context));
    EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context));
    EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context));
    EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
}

// The compatibility-profile accessor is affirmative-only (it backs GL_CONTEXT_PROFILE_MASK
// reporting): attrib-less contexts (profile mask 0) and released threads both answer "not
// compat" and therefore read as core-profile contexts.
TEST(EGLStateProfile, CompatibilityProfileRequiresExplicitCompatBit) {
    auto fixture = CreateFixture();

    EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());

    EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context));
    EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());

    const EGLint compatAttribs[] = {EGL_CONTEXT_MAJOR_VERSION,
                                    3,
                                    EGL_CONTEXT_MINOR_VERSION,
                                    3,
                                    EGL_CONTEXT_OPENGL_PROFILE_MASK,
                                    EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
                                    EGL_NONE};
    const auto compatContext =
        fixture->State.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, compatAttribs);
    ASSERT_NE(compatContext, EGL_NO_CONTEXT);
    EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, compatContext));
    EXPECT_TRUE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
    EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCoreProfile());

    EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
    EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
    EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
}
