/*
 * Copyright © Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 or 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <mir/events/event_builders.h>
#include <mir/events/close_window_event.h>
#include <mir/events/resize_event.h>
#include <mir/scene/basic_surface.h>
#include <mir/scene/null_surface_observer.h>
#include "src/server/report/null_report_factory.h"
#include <mir/scene/output_properties_cache.h>

#include <mir/test/doubles/fake_display_configuration_observer_registrar.h>
#include <mir/test/doubles/mock_buffer_stream.h>
#include <mir/test/doubles/mock_event_sink.h>
#include <mir/test/doubles/explicit_executor.h>
#include <mir/test/doubles/test_surface_observer.h>
#include <mir/test/fake_shared.h>
#include <mir/test/event_matchers.h>

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

namespace ms = mir::scene;
namespace msh = mir::shell;
namespace mf = mir::frontend;
namespace mw = mir::wayland;
namespace mc = mir::compositor;
namespace mg = mir::graphics;
namespace mi = mir::input;
namespace mev = mir::events;
namespace geom = mir::geometry;
namespace mt = mir::test;
namespace mtd = mt::doubles;
namespace mr = mir::report;

namespace
{

MATCHER_P(MirResizeEventEq, event, "")
{
    if (arg.type() != mir_event_type_resize ||
        event->type() != mir_event_type_resize)
    {
        return false;
    }

    return arg.to_resize()->width() == event->to_resize()->width() &&
           arg.to_resize()->height() == event->to_resize()->height() &&
           arg.to_resize()->surface_id() == event->to_resize()->surface_id();
}

typedef testing::NiceMock<mtd::MockBufferStream> StubBufferStream;

struct Surface : testing::Test
{
    std::shared_ptr<StubBufferStream> const buffer_stream = std::make_shared<StubBufferStream>();

    void SetUp()
    {
        using namespace testing;

        surface = std::make_shared<ms::BasicSurface>(
            std::string("stub"),
            geom::Rectangle{{},{}},
            mir_pointer_unconfined,
            std::list<ms::StreamInfo> { { buffer_stream, {}} },
            nullptr,
            report,
            display_config_registrar);
    }

    mf::SurfaceId stub_id;
    std::shared_ptr<ms::SceneReport> const report = mr::null_scene_report();
    std::shared_ptr<ms::BasicSurface> surface;
    std::shared_ptr<mtd::FakeDisplayConfigurationObserverRegistrar> const display_config_registrar =
        std::make_shared<mtd::FakeDisplayConfigurationObserverRegistrar>();
};
}

TEST_F(Surface, attributes)
{
    using namespace testing;

    EXPECT_THROW({
        surface->configure(static_cast<MirWindowAttrib>(111), 222);
    }, std::logic_error);
}

TEST_F(Surface, types)
{
    using namespace testing;

    EXPECT_EQ(mir_window_type_normal, surface->type());

    EXPECT_EQ(mir_window_type_utility,
              surface->configure(mir_window_attrib_type,
                             mir_window_type_utility));
    EXPECT_EQ(mir_window_type_utility, surface->type());

    EXPECT_THROW({
        surface->configure(mir_window_attrib_type, 999);
    }, std::logic_error);
    EXPECT_THROW({
        surface->configure(mir_window_attrib_type, -1);
    }, std::logic_error);
    EXPECT_EQ(mir_window_type_utility, surface->type());

    EXPECT_EQ(mir_window_type_dialog,
              surface->configure(mir_window_attrib_type,
                             mir_window_type_dialog));
    EXPECT_EQ(mir_window_type_dialog, surface->type());

    EXPECT_EQ(mir_window_type_freestyle,
              surface->configure(mir_window_attrib_type,
                             mir_window_type_freestyle));
    EXPECT_EQ(mir_window_type_freestyle, surface->type());
}

TEST_F(Surface, states)
{
    using namespace testing;

    EXPECT_EQ(mir_window_state_restored, surface->state());

    EXPECT_EQ(mir_window_state_vertmaximized,
              surface->configure(mir_window_attrib_state,
                             mir_window_state_vertmaximized));
    EXPECT_EQ(mir_window_state_vertmaximized, surface->state());

    EXPECT_THROW({
        surface->configure(mir_window_attrib_state, 999);
    }, std::logic_error);
    EXPECT_THROW({
        surface->configure(mir_window_attrib_state, -1);
    }, std::logic_error);
    EXPECT_EQ(mir_window_state_vertmaximized, surface->state());

    EXPECT_EQ(mir_window_state_minimized,
              surface->configure(mir_window_attrib_state,
                             mir_window_state_minimized));
    EXPECT_EQ(mir_window_state_minimized, surface->state());

    EXPECT_EQ(mir_window_state_fullscreen,
              surface->configure(mir_window_attrib_state,
                             mir_window_state_fullscreen));
    EXPECT_EQ(mir_window_state_fullscreen, surface->state());
}

TEST_F(Surface, clamps_undersized_resize)
{
    using namespace testing;

    geom::Size const try_size{-123, -456};
    geom::Size const expect_size{1, 1};

    surface->resize(try_size);
    EXPECT_EQ(expect_size, surface->window_size());
}

TEST_F(Surface, emits_resize_events)
{
    using namespace testing;

    geom::Size const new_size{123, 456};
    auto sink = std::make_shared<mtd::MockEventSink>();
    auto const observer = std::make_shared<mtd::TestSurfaceObserver>(stub_id, sink);

    mtd::ExplicitExecutor executor;
    surface->register_interest(observer, executor);

    auto e = mev::make_window_resize_event(stub_id, new_size);
    EXPECT_CALL(*sink, handle_event(MirResizeEventEq(e.get())))
        .Times(1);

    surface->resize(new_size);
    executor.execute();
    EXPECT_EQ(new_size, surface->window_size());
}

TEST_F(Surface, emits_resize_events_only_on_change)
{
    using namespace testing;

    geom::Size const new_size{123, 456};
    geom::Size const new_size2{789, 1011};
    auto sink = std::make_shared<mtd::MockEventSink>();
    auto const observer = std::make_shared<mtd::TestSurfaceObserver>(stub_id, sink);

    mtd::ExplicitExecutor executor;
    surface->register_interest(observer, executor);

    auto e = mev::make_window_resize_event(stub_id, new_size);
    EXPECT_CALL(*sink, handle_event(MirResizeEventEq(e.get())))
        .Times(1);

    auto e2 = mev::make_window_resize_event(stub_id, new_size2);
    EXPECT_CALL(*sink, handle_event(MirResizeEventEq(e2.get())))
        .Times(1);

    surface->resize(new_size);
    EXPECT_EQ(new_size, surface->window_size());
    surface->resize(new_size);
    EXPECT_EQ(new_size, surface->window_size());

    surface->resize(new_size2);
    EXPECT_EQ(new_size2, surface->window_size());
    surface->resize(new_size2);
    EXPECT_EQ(new_size2, surface->window_size());

    executor.execute();
}

TEST_F(Surface, sends_focus_notifications_when_focus_gained_and_lost)
{
    using namespace testing;

    mtd::MockEventSink sink;

    {
        InSequence seq;
        EXPECT_CALL(sink, handle_event(mt::WindowEvent(mir_window_attrib_focus, mir_window_focus_state_focused)))
            .Times(1);
        EXPECT_CALL(sink, handle_event(mt::WindowEvent(mir_window_attrib_focus, mir_window_focus_state_unfocused)))
            .Times(1);
    }

    auto const observer = std::make_shared<mtd::TestSurfaceObserver>(stub_id, mt::fake_shared(sink));

    mtd::ExplicitExecutor executor;
    surface->register_interest(observer, executor);

    surface->configure(mir_window_attrib_focus, mir_window_focus_state_focused);
    surface->configure(mir_window_attrib_focus, mir_window_focus_state_unfocused);
    executor.execute();
}
MATCHER_P(MirCloseSurfaceEventMatches, event, "")
{
    return arg.type() == event->type();
}

TEST_F(Surface, emits_client_close_events)
{
    using namespace testing;

    auto sink = std::make_shared<mtd::MockEventSink>();
    auto const observer = std::make_shared<mtd::TestSurfaceObserver>(stub_id, sink);

    mtd::ExplicitExecutor executor;
    surface->register_interest(observer, executor);

    MirCloseWindowEvent e;
    e.to_close_window()->set_surface_id(stub_id.as_value());

    EXPECT_CALL(*sink, handle_event(MirCloseSurfaceEventMatches(&e))).Times(1);

    surface->request_client_surface_close();
    executor.execute();
}

TEST_F(Surface, preferred_orientation_mode_defaults_to_any)
{
    using namespace testing;

    ms::BasicSurface surf(
        std::string("stub"),
        geom::Rectangle{{},{}},
        mir_pointer_unconfined,
        std::list<ms::StreamInfo> { { buffer_stream, {}} },
        std::shared_ptr<mg::CursorImage>(),
        report,
        display_config_registrar);

    EXPECT_EQ(mir_orientation_mode_any, surf.query(mir_window_attrib_preferred_orientation));
}
