// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/page_image_annotation/core/page_annotator.h"

#include <vector>

#include "base/bind.h"
#include "base/test/scoped_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace page_image_annotation {

namespace {

namespace ia_mojom = image_annotation::mojom;

using testing::ElementsAre;
using testing::Eq;
using testing::NiceMock;
using testing::SizeIs;

// A gMock matcher that compares an ImageMetadata to the given node and source
// IDs.
MATCHER_P2(IsImageMetadata, expected_node_id, expected_source_id, "") {
  return arg.node_id == expected_node_id && arg.source_id == expected_source_id;
}

// A gMock matcher that (deep) compares an AnnotateImageResultPtr to the
// expected result (which is of type AnnotateImageResult*).
MATCHER_P(IsAnnotateImageResult, expected, "") {
  return arg->Equals(*expected);
}

class MockObserver : public PageAnnotator::Observer {
 public:
  MOCK_METHOD1(OnImageAdded,
               void(const PageAnnotator::ImageMetadata& metadata));
  MOCK_METHOD1(OnImageModified,
               void(const PageAnnotator::ImageMetadata& metadata));
  MOCK_METHOD1(OnImageRemoved, void(uint64_t node_id));
  MOCK_METHOD2(OnImageAnnotated,
               void(uint64_t node_id, ia_mojom::AnnotateImageResultPtr result));
};

// An annotator that just stores and exposes the arguments with which its
// AnnotateImage method was called.
class TestAnnotator : public ia_mojom::Annotator {
 public:
  ia_mojom::AnnotatorPtr GetPtr() {
    ia_mojom::AnnotatorPtr ptr;
    bindings_.AddBinding(this, mojo::MakeRequest(&ptr));
    return ptr;
  }

  void AnnotateImage(const std::string& source_id,
                     const std::string& description_language_tag,
                     ia_mojom::ImageProcessorPtr image_processor,
                     AnnotateImageCallback callback) override {
    CHECK_EQ(description_language_tag, std::string());

    source_ids_.push_back(source_id);

    image_processors_.push_back(std::move(image_processor));
    image_processors_.back().set_connection_error_handler(
        base::BindOnce(&TestAnnotator::ResetImageProcessor,
                       base::Unretained(this), image_processors_.size() - 1));

    callbacks_.push_back(std::move(callback));
  }

  // Tests should not delete entries in these lists.
  std::vector<std::string> source_ids_;
  std::vector<ia_mojom::ImageProcessorPtr> image_processors_;
  std::vector<AnnotateImageCallback> callbacks_;

 private:
  void ResetImageProcessor(const size_t index) {
    image_processors_[index].reset();
  }

  mojo::BindingSet<ia_mojom::Annotator> bindings_;
};

// Tests that correct image tracking messages are sent to observers.
TEST(PageAnnotatorTest, ImageTracking) {
  const auto get_pixels = base::BindRepeating([]() { return SkBitmap(); });

  base::test::ScopedTaskEnvironment test_task_env;

  PageAnnotator page_annotator(ia_mojom::AnnotatorPtr{});

  MockObserver o1;
  page_annotator.AddObserver(&o1);

  EXPECT_CALL(o1, OnImageAdded(IsImageMetadata(1ul, "test.jpg")));
  page_annotator.ImageAddedOrPossiblyModified({1ul, "test.jpg"}, get_pixels);

  EXPECT_CALL(o1, OnImageAdded(IsImageMetadata(2ul, "example.png")));
  page_annotator.ImageAddedOrPossiblyModified({2ul, "example.png"}, get_pixels);

  EXPECT_CALL(o1, OnImageModified(IsImageMetadata(1ul, "demo.gif")));
  page_annotator.ImageAddedOrPossiblyModified({1ul, "demo.gif"}, get_pixels);

  EXPECT_CALL(o1, OnImageRemoved(2ul));
  page_annotator.ImageRemoved(2ul);

  MockObserver o2;
  EXPECT_CALL(o2, OnImageAdded(IsImageMetadata(1ul, "demo.gif")));
  page_annotator.AddObserver(&o2);
}

// Tests service and observer communication when performing image annotation.
TEST(PageAnnotatorTest, Annotation) {
  // Returning a null SkBitmap is ok as long as we don't request JPG data from
  // the local ImageProcessor.
  const auto get_pixels = base::BindRepeating([]() { return SkBitmap(); });

  base::test::ScopedTaskEnvironment test_task_env;

  TestAnnotator test_annotator;
  PageAnnotator page_annotator(test_annotator.GetPtr());
  test_task_env.RunUntilIdle();

  // We use NiceMocks here since we don't place expectations on image added /
  // removed calls, which will otherwise cause many (benign) warnings to be
  // logged.
  NiceMock<MockObserver> o1, o2;
  page_annotator.AddObserver(&o1);
  page_annotator.AddObserver(&o2);

  // First image added.
  page_annotator.ImageAddedOrPossiblyModified({1ul, "test.jpg"}, get_pixels);

  // Observer 1 requests annotation of the first image.
  page_annotator.AnnotateImage(&o1, 1ul);
  test_task_env.RunUntilIdle();

  // The annotator should have been provided observer 1's request info.
  EXPECT_THAT(test_annotator.source_ids_, ElementsAre("test.jpg"));
  ASSERT_THAT(test_annotator.image_processors_, SizeIs(1));
  EXPECT_THAT(test_annotator.image_processors_[0].is_bound(), Eq(true));
  EXPECT_THAT(test_annotator.callbacks_, SizeIs(1));

  // Observer 2 requests annotation of the same image.
  page_annotator.AnnotateImage(&o2, 1ul);
  test_task_env.RunUntilIdle();

  // The annotator should have been provided observer 2's request info.
  EXPECT_THAT(test_annotator.source_ids_, ElementsAre("test.jpg", "test.jpg"));
  ASSERT_THAT(test_annotator.image_processors_, SizeIs(2));
  EXPECT_THAT(test_annotator.image_processors_[0].is_bound(), Eq(true));
  EXPECT_THAT(test_annotator.image_processors_[1].is_bound(), Eq(true));
  EXPECT_THAT(test_annotator.callbacks_, SizeIs(2));

  // Second image added.
  page_annotator.ImageAddedOrPossiblyModified({2ul, "example.png"}, get_pixels);

  // Observer 2 requests annotation of the second image.
  page_annotator.AnnotateImage(&o2, 2ul);
  test_task_env.RunUntilIdle();

  // All three requests should have been provided to the annotator.
  EXPECT_THAT(test_annotator.source_ids_,
              ElementsAre("test.jpg", "test.jpg", "example.png"));
  ASSERT_THAT(test_annotator.image_processors_, SizeIs(3));
  EXPECT_THAT(test_annotator.image_processors_[0].is_bound(), Eq(true));
  EXPECT_THAT(test_annotator.image_processors_[1].is_bound(), Eq(true));
  EXPECT_THAT(test_annotator.image_processors_[2].is_bound(), Eq(true));
  EXPECT_THAT(test_annotator.callbacks_, SizeIs(3));

  // Image 1 goes away.
  page_annotator.ImageRemoved(1ul);
  test_task_env.RunUntilIdle();

  // The corresponding image processors should have been disconnected.
  ASSERT_THAT(test_annotator.image_processors_, SizeIs(3));
  EXPECT_THAT(test_annotator.image_processors_[0].is_bound(), Eq(false));
  EXPECT_THAT(test_annotator.image_processors_[1].is_bound(), Eq(false));
  EXPECT_THAT(test_annotator.image_processors_[2].is_bound(), Eq(true));

  // Expect success and failure to be reported.
  const auto error = ia_mojom::AnnotateImageResult::NewErrorCode(
      ia_mojom::AnnotateImageError::kCanceled);

  // Can't use an initializer list since it performs copies.
  std::vector<ia_mojom::AnnotationPtr> annotations;
  annotations.push_back(ia_mojom::Annotation::New(
      ia_mojom::AnnotationType::kOcr, 1.0, "text from image"));
  const auto success =
      ia_mojom::AnnotateImageResult::NewAnnotations(std::move(annotations));

  ASSERT_THAT(test_annotator.callbacks_, SizeIs(3));
  std::move(test_annotator.callbacks_[0]).Run(error.Clone());
  std::move(test_annotator.callbacks_[1]).Run(error.Clone());
  std::move(test_annotator.callbacks_[2]).Run(success.Clone());

  EXPECT_CALL(o1, OnImageAnnotated(1ul, IsAnnotateImageResult(error.get())));
  EXPECT_CALL(o2, OnImageAnnotated(1ul, IsAnnotateImageResult(error.get())));
  EXPECT_CALL(o2, OnImageAnnotated(2ul, IsAnnotateImageResult(success.get())));

  test_task_env.RunUntilIdle();
}

}  // namespace

}  // namespace page_image_annotation
