// Copyright 2010-2020, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "renderer/unix/infolist_window.h"

#include <string>

#include "renderer/unix/cairo_factory_mock.h"
#include "renderer/unix/const.h"
#include "renderer/unix/draw_tool_mock.h"
#include "renderer/unix/gtk_wrapper_mock.h"
#include "renderer/unix/text_renderer_mock.h"
#include "testing/base/public/gmock.h"
#include "testing/base/public/gunit.h"

using ::testing::_;
using ::testing::Return;
using ::testing::StrEq;
using ::testing::StrictMock;

namespace mozc {
namespace renderer {
namespace gtk {

namespace {
// Following variable is used testing and it is contant but due to API
// restriction, can not modify const modifier.
GtkWidget *kDummyWindow = reinterpret_cast<GtkWidget *>(0x12345678);
GtkWidget *kDummyCanvas = reinterpret_cast<GtkWidget *>(0x87654321);

const char kSampleTitle[] = "TITLE";
const char kSampleDescription[] = "DESCRIPTION";

void SetInformations(int count, commands::InformationList *usages) {
  usages->Clear();
  for (int i = 0; i < count; ++i) {
    commands::Information *info = usages->add_information();

    const string id_str = std::to_string(i);
    info->set_title(kSampleTitle + id_str);
    info->set_description(kSampleDescription + id_str);
  }
}

string GetExpectedTitle(int row) {
  const string id_str = std::to_string(row);
  return kSampleTitle + id_str;
}

string GetExpectedDescription(int row) {
  const string id_str = std::to_string(row);
  return kSampleDescription + id_str;
}

MATCHER_P(RectEq, expected_rect, "The expected rect does not match") {
  return (arg.origin.x == expected_rect.origin.x) &&
         (arg.origin.y == expected_rect.origin.y) &&
         (arg.size.width == expected_rect.size.width) &&
         (arg.size.height == expected_rect.size.height);
}

MATCHER_P(RGBAEq, expected_rgba, "The expected RGBA does not match") {
  return (arg.red == expected_rgba.red) && (arg.green == expected_rgba.green) &&
         (arg.blue == expected_rgba.blue) && (arg.alpha == expected_rgba.alpha);
}

RGBA StyleColorToRGBA(const RendererStyle::RGBAColor &rgbacolor) {
  const RGBA rgba = {static_cast<uint8>(rgbacolor.r()),
                     static_cast<uint8>(rgbacolor.g()),
                     static_cast<uint8>(rgbacolor.b()), 0xFF};
  return rgba;
}
}  // namespace

class InfolistWindowTest : public testing::Test {
 protected:
  struct InfolistWindowTestKit {
    GtkWrapperMock *gtk_mock;
    TextRendererMock *text_renderer_mock;
    DrawToolMock *draw_tool_mock;
    CairoFactoryMock *cairo_factory_mock;
    InfolistWindow *window;
  };

  void SetUpInfolistWindowConstractorCallExpectations(
      InfolistWindowTestKit *testkit) {
    // Following functions are expected to be called by constructor.
    EXPECT_CALL(*testkit->gtk_mock, GtkWindowNew(GTK_WINDOW_POPUP))
        .WillOnce(Return(kDummyWindow));
    EXPECT_CALL(*testkit->gtk_mock, GtkDrawingAreaNew())
        .WillOnce(Return(kDummyCanvas));
    EXPECT_CALL(*testkit->gtk_mock,
                GSignalConnect(kDummyWindow, StrEq("destroy"),
                               G_CALLBACK(GtkWindowBase::OnDestroyThunk), _));
    EXPECT_CALL(*testkit->gtk_mock,
                GSignalConnect(kDummyWindow, StrEq("button-press-event"),
                               G_CALLBACK(GtkWindowBase::OnMouseDownThunk), _));
    EXPECT_CALL(*testkit->gtk_mock,
                GSignalConnect(kDummyWindow, StrEq("button-release-event"),
                               G_CALLBACK(GtkWindowBase::OnMouseUpThunk), _));
    EXPECT_CALL(*testkit->gtk_mock,
                GSignalConnect(kDummyCanvas, StrEq("expose-event"),
                               G_CALLBACK(GtkWindowBase::OnPaintThunk), _));
    EXPECT_CALL(*testkit->gtk_mock,
                GtkContainerAdd(kDummyWindow, kDummyCanvas));
    EXPECT_CALL(*testkit->gtk_mock,
                GtkWidgetAddEvents(kDummyWindow, GDK_BUTTON_PRESS_MASK));
    EXPECT_CALL(*testkit->gtk_mock,
                GtkWidgetAddEvents(kDummyWindow, GDK_BUTTON_RELEASE_MASK));
    EXPECT_CALL(*testkit->gtk_mock, GtkWidgetRealize(kDummyWindow));
    EXPECT_CALL(
        *testkit->gtk_mock,
        GdkWindowSetTypeHint(kDummyWindow, GDK_WINDOW_TYPE_HINT_POPUP_MENU));
  }

  InfolistWindowTestKit SetUpInfolistWindow() {
    InfolistWindowTestKit testkit;
    testkit.gtk_mock = new GtkWrapperMock();
    testkit.text_renderer_mock = new TextRendererMock();
    testkit.draw_tool_mock = new DrawToolMock();
    testkit.cairo_factory_mock = new CairoFactoryMock();

    SetUpInfolistWindowConstractorCallExpectations(&testkit);

    testkit.window =
        new InfolistWindow(testkit.text_renderer_mock, testkit.draw_tool_mock,
                           testkit.gtk_mock, testkit.cairo_factory_mock);
    return testkit;
  }

  InfolistWindowTestKit SetUpInfolistWindowWithStrictMock() {
    InfolistWindowTestKit testkit;
    testkit.gtk_mock = new StrictMock<GtkWrapperMock>();
    testkit.text_renderer_mock = new StrictMock<TextRendererMock>();
    testkit.draw_tool_mock = new StrictMock<DrawToolMock>();
    testkit.cairo_factory_mock = new StrictMock<CairoFactoryMock>();

    SetUpInfolistWindowConstractorCallExpectations(&testkit);

    testkit.window =
        new InfolistWindow(testkit.text_renderer_mock, testkit.draw_tool_mock,
                           testkit.gtk_mock, testkit.cairo_factory_mock);
    return testkit;
  }

  void FinalizeTestKit(InfolistWindowTestKit *testkit) {
    delete testkit->window;
  }
};

TEST_F(InfolistWindowTest, DrawFrameTest) {
  InfolistWindowTestKit testkit = SetUpInfolistWindow();
  const RendererStyle::InfolistStyle &infostyle =
      testkit.window->style_->infolist_style();
  const int height = 1234;
  const Rect expected_rect(0, 0, infostyle.window_width(), height);
  EXPECT_CALL(*testkit.draw_tool_mock,
              FrameRect(RectEq(expected_rect),
                        RGBAEq(StyleColorToRGBA(infostyle.border_color())), 1));
  testkit.window->DrawFrame(height);
  FinalizeTestKit(&testkit);
}

TEST_F(InfolistWindowTest, GetRowRectsTest) {
  InfolistWindowTestKit testkit = SetUpInfolistWindow();
  const RendererStyle::InfolistStyle &infostyle =
      testkit.window->style_->infolist_style();
  const renderer::RendererStyle::TextStyle title_style =
      infostyle.title_style();
  const renderer::RendererStyle::TextStyle description_style =
      infostyle.description_style();
  const int ypos = 123;
  const FontSpecInterface::FONT_TYPE infolist_font_type =
      FontSpecInterface::FONTSET_INFOLIST_TITLE;
  const FontSpecInterface::FONT_TYPE description_font_type =
      FontSpecInterface::FONTSET_INFOLIST_DESCRIPTION;
  const Size text_size(10, 20);

  SetInformations(10, testkit.window->candidates_.mutable_usages());

  EXPECT_CALL(*testkit.text_renderer_mock,
              GetMultiLinePixelSize(infolist_font_type, GetExpectedTitle(0), _))
      .WillOnce(Return(text_size));
  EXPECT_CALL(*testkit.text_renderer_mock,
              GetMultiLinePixelSize(description_font_type,
                                    GetExpectedDescription(0), _))
      .WillOnce(Return(text_size));

  InfolistWindow::RenderingRowRects row_rects =
      testkit.window->GetRowRects(0, ypos);

  EXPECT_EQ(row_rects.title_back_rect.Height(),
            row_rects.desc_back_rect.Top() - row_rects.title_back_rect.Top());
  EXPECT_EQ(row_rects.title_back_rect.Top(), row_rects.whole_rect.Top());
  EXPECT_EQ(row_rects.title_back_rect.Left(), row_rects.whole_rect.Left());
  EXPECT_EQ(row_rects.title_back_rect.Width(), row_rects.whole_rect.Width());
  EXPECT_EQ(
      row_rects.title_back_rect.Height() + row_rects.desc_back_rect.Height(),
      row_rects.whole_rect.Height());

  FinalizeTestKit(&testkit);
}

TEST_F(InfolistWindowTest, DrawRowTest) {
  {
    SCOPED_TRACE("Draw focused area");
    for (int i = 0; i < 10; ++i) {
      InfolistWindowTestKit testkit = SetUpInfolistWindow();
      const RendererStyle::InfolistStyle &infostyle =
          testkit.window->style_->infolist_style();
      const FontSpecInterface::FONT_TYPE infolist_font_type =
          FontSpecInterface::FONTSET_INFOLIST_TITLE;
      const FontSpecInterface::FONT_TYPE description_font_type =
          FontSpecInterface::FONTSET_INFOLIST_DESCRIPTION;
      const Size text_size(10, 20);
      SetInformations(10, testkit.window->candidates_.mutable_usages());
      const int ypos = i * 15;

      testkit.window->candidates_.mutable_usages()->set_focused_index(i);

      EXPECT_CALL(
          *testkit.text_renderer_mock,
          GetMultiLinePixelSize(infolist_font_type, GetExpectedTitle(i), _))
          .WillRepeatedly(Return(text_size));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  GetMultiLinePixelSize(description_font_type,
                                        GetExpectedDescription(i), _))
          .WillRepeatedly(Return(text_size));

      const InfolistWindow::RenderingRowRects sample_row_rects =
          testkit.window->GetRowRects(i, ypos);

      EXPECT_CALL(
          *testkit.draw_tool_mock,
          FillRect(
              RectEq(sample_row_rects.whole_rect),
              RGBAEq(StyleColorToRGBA(infostyle.focused_background_color()))));
      EXPECT_CALL(
          *testkit.draw_tool_mock,
          FrameRect(RectEq(sample_row_rects.whole_rect),
                    RGBAEq(StyleColorToRGBA(infostyle.focused_border_color())),
                    1));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  RenderText(StrEq(GetExpectedTitle(i)),
                             RectEq(sample_row_rects.title_rect),
                             FontSpecInterface::FONTSET_INFOLIST_TITLE));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  RenderText(StrEq(GetExpectedDescription(i)),
                             RectEq(sample_row_rects.desc_rect),
                             FontSpecInterface::FONTSET_INFOLIST_DESCRIPTION));

      testkit.window->DrawRow(i, ypos);
      FinalizeTestKit(&testkit);
    }
  }
  {
    SCOPED_TRACE("Draw focus-outed area with default color");
    for (int i = 0; i < 10; ++i) {
      InfolistWindowTestKit testkit = SetUpInfolistWindow();
      RendererStyle::InfolistStyle *infostyle =
          testkit.window->style_->mutable_infolist_style();
      infostyle->mutable_title_style()->clear_background_color();
      infostyle->mutable_description_style()->clear_background_color();
      const FontSpecInterface::FONT_TYPE infolist_font_type =
          FontSpecInterface::FONTSET_INFOLIST_TITLE;
      const FontSpecInterface::FONT_TYPE description_font_type =
          FontSpecInterface::FONTSET_INFOLIST_DESCRIPTION;
      const Size text_size(10, 20);
      SetInformations(10, testkit.window->candidates_.mutable_usages());
      const int ypos = i * 15;
      SetInformations(10, testkit.window->candidates_.mutable_usages());

      EXPECT_CALL(
          *testkit.text_renderer_mock,
          GetMultiLinePixelSize(infolist_font_type, GetExpectedTitle(i), _))
          .WillRepeatedly(Return(text_size));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  GetMultiLinePixelSize(description_font_type,
                                        GetExpectedDescription(i), _))
          .WillRepeatedly(Return(text_size));

      const InfolistWindow::RenderingRowRects sample_row_rects =
          testkit.window->GetRowRects(i, ypos);

      EXPECT_CALL(
          *testkit.draw_tool_mock,
          FillRect(RectEq(sample_row_rects.title_back_rect), RGBAEq(kWhite)));
      EXPECT_CALL(
          *testkit.draw_tool_mock,
          FillRect(RectEq(sample_row_rects.desc_back_rect), RGBAEq(kWhite)));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  RenderText(StrEq(GetExpectedTitle(i)),
                             RectEq(sample_row_rects.title_rect),
                             FontSpecInterface::FONTSET_INFOLIST_TITLE));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  RenderText(StrEq(GetExpectedDescription(i)),
                             RectEq(sample_row_rects.desc_rect),
                             FontSpecInterface::FONTSET_INFOLIST_DESCRIPTION));

      testkit.window->DrawRow(i, ypos);
      FinalizeTestKit(&testkit);
    }
  }
  {
    SCOPED_TRACE("Draw focus-outed area with specified color");
    for (int i = 0; i < 10; ++i) {
      InfolistWindowTestKit testkit = SetUpInfolistWindow();
      RendererStyle::InfolistStyle *infostyle =
          testkit.window->style_->mutable_infolist_style();
      infostyle->mutable_title_style()->mutable_background_color();
      infostyle->mutable_description_style()->mutable_background_color();
      const FontSpecInterface::FONT_TYPE infolist_font_type =
          FontSpecInterface::FONTSET_INFOLIST_TITLE;
      const FontSpecInterface::FONT_TYPE description_font_type =
          FontSpecInterface::FONTSET_INFOLIST_DESCRIPTION;
      const Size text_size(10, 20);
      SetInformations(10, testkit.window->candidates_.mutable_usages());
      const int ypos = i * 15;

      SetInformations(10, testkit.window->candidates_.mutable_usages());

      EXPECT_CALL(
          *testkit.text_renderer_mock,
          GetMultiLinePixelSize(infolist_font_type, GetExpectedTitle(i), _))
          .WillRepeatedly(Return(text_size));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  GetMultiLinePixelSize(description_font_type,
                                        GetExpectedDescription(i), _))
          .WillRepeatedly(Return(text_size));

      const InfolistWindow::RenderingRowRects sample_row_rects =
          testkit.window->GetRowRects(i, ypos);

      EXPECT_CALL(*testkit.draw_tool_mock,
                  FillRect(RectEq(sample_row_rects.title_back_rect),
                           RGBAEq(StyleColorToRGBA(
                               infostyle->title_style().background_color()))));
      EXPECT_CALL(
          *testkit.draw_tool_mock,
          FillRect(RectEq(sample_row_rects.desc_back_rect),
                   RGBAEq(StyleColorToRGBA(
                       infostyle->description_style().background_color()))));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  RenderText(StrEq(GetExpectedTitle(i)),
                             RectEq(sample_row_rects.title_rect),
                             FontSpecInterface::FONTSET_INFOLIST_TITLE));
      EXPECT_CALL(*testkit.text_renderer_mock,
                  RenderText(StrEq(GetExpectedDescription(i)),
                             RectEq(sample_row_rects.desc_rect),
                             FontSpecInterface::FONTSET_INFOLIST_DESCRIPTION));

      testkit.window->DrawRow(i, ypos);
      FinalizeTestKit(&testkit);
    }
  }
}

TEST_F(InfolistWindowTest, DrawCaptionTest) {
  {
    SCOPED_TRACE("If there is no caption, do nothing");
    InfolistWindowTestKit testkit = SetUpInfolistWindowWithStrictMock();
    testkit.window->style_.reset(new RendererStyle());
    testkit.window->DrawCaption();
    FinalizeTestKit(&testkit);
  }
  {
    InfolistWindowTestKit testkit = SetUpInfolistWindow();
    const RendererStyle::InfolistStyle &infostyle =
        testkit.window->style_->infolist_style();
    const RendererStyle::TextStyle &caption_style = infostyle.caption_style();

    const Rect bg_expected_rect(
        infostyle.window_border(), infostyle.window_border(),
        infostyle.window_width() - infostyle.window_border() * 2,
        infostyle.caption_height());
    const RGBA bg_expected_color =
        StyleColorToRGBA(infostyle.caption_background_color());
    EXPECT_CALL(*testkit.draw_tool_mock,
                FillRect(RectEq(bg_expected_rect), RGBAEq(bg_expected_color)));

    const Rect caption_expected_rect(
        bg_expected_rect.Left() + infostyle.caption_padding() +
            caption_style.left_padding(),
        bg_expected_rect.Top() + infostyle.caption_padding(),
        bg_expected_rect.Width() - infostyle.caption_padding() -
            caption_style.left_padding(),
        infostyle.caption_height() - infostyle.caption_padding());
    EXPECT_CALL(
        *testkit.text_renderer_mock,
        RenderText(infostyle.caption_string(), RectEq(caption_expected_rect),
                   FontSpecInterface::FONTSET_INFOLIST_CAPTION));

    EXPECT_EQ(infostyle.caption_height(), testkit.window->DrawCaption());

    FinalizeTestKit(&testkit);
  }
}

TEST_F(InfolistWindowTest, GetRenderingRectsTest) {
  // TODO(nona): rectangle argument verification.
  {
    SCOPED_TRACE("title style");
    InfolistWindowTestKit testkit = SetUpInfolistWindow();
    const RendererStyle::InfolistStyle &infostyle =
        testkit.window->style_->infolist_style();
    const renderer::RendererStyle::TextStyle title_style =
        infostyle.title_style();
    const int ypos = 123;
    const FontSpecInterface::FONT_TYPE font_type =
        FontSpecInterface::FONTSET_INFOLIST_TITLE;
    const Size text_size(10, 20);
    EXPECT_CALL(*testkit.text_renderer_mock,
                GetMultiLinePixelSize(font_type, kSampleTitle, _))
        .WillOnce(Return(text_size));

    Rect bg_rect, text_rect;
    testkit.window->GetRenderingRects(title_style, kSampleTitle, font_type,
                                      ypos, &bg_rect, &text_rect);
    FinalizeTestKit(&testkit);
  }
  {
    SCOPED_TRACE("description style");
    InfolistWindowTestKit testkit = SetUpInfolistWindow();
    const RendererStyle::InfolistStyle &infostyle =
        testkit.window->style_->infolist_style();
    const renderer::RendererStyle::TextStyle description_style =
        infostyle.description_style();
    const int ypos = 234;
    const FontSpecInterface::FONT_TYPE font_type =
        FontSpecInterface::FONTSET_INFOLIST_DESCRIPTION;
    const Size text_size(10, 20);
    EXPECT_CALL(*testkit.text_renderer_mock,
                GetMultiLinePixelSize(font_type, kSampleDescription, _))
        .WillOnce(Return(text_size));

    Rect bg_rect, text_rect;
    testkit.window->GetRenderingRects(description_style, kSampleDescription,
                                      font_type, ypos, &bg_rect, &text_rect);
    FinalizeTestKit(&testkit);
  }
}

TEST_F(InfolistWindowTest, ReloadFontConfigTest) {
  InfolistWindowTestKit testkit = SetUpInfolistWindow();
  const char kDummyFontDescription[] = "Foo,Bar,Baz";
  EXPECT_CALL(*testkit.text_renderer_mock,
              ReloadFontConfig(StrEq(kDummyFontDescription)));
  testkit.window->ReloadFontConfig(kDummyFontDescription);
  FinalizeTestKit(&testkit);
}

}  // namespace gtk
}  // namespace renderer
}  // namespace mozc
