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

#include "content/public/test/fake_speech_recognition_manager.h"

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/speech_recognition_event_listener.h"
#include "content/public/browser/speech_recognition_manager_delegate.h"
#include "content/public/test/test_utils.h"
#include "media/mojo/mojom/speech_recognition_error.mojom.h"
#include "media/mojo/mojom/speech_recognition_result.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {
const char kTestResult[] = "Pictures of the moon";
}  // namespace

namespace content {

FakeSpeechRecognitionManager::FakeSpeechRecognitionManager()
    : session_id_(0), listener_(nullptr), fake_result_(kTestResult) {}

void FakeSpeechRecognitionManager::SetDelegate(
    SpeechRecognitionManagerDelegate* delegate) {
  delegate_ = delegate;
}

FakeSpeechRecognitionManager::~FakeSpeechRecognitionManager() {
  // Expect the owner of |delegate_| to cleanup our reference before we shut
  // down, just to be safe as we do not own |delegate_|.
  DCHECK(!delegate_);
}

void FakeSpeechRecognitionManager::WaitForRecognitionStarted() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  base::RunLoop runner;
  recognition_started_closure_ = runner.QuitClosure();
  runner.Run();
}

void FakeSpeechRecognitionManager::WaitForRecognitionEnded() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Take no action if recognition is not currently running.
  if (session_id_ == 0)
    return;
  base::RunLoop runner;
  recognition_ended_closure_ = runner.QuitClosure();
  runner.Run();
}

void FakeSpeechRecognitionManager::SendFakeResponse(
    bool end_recognition,
    base::OnceClosure on_fake_response_sent) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // The session must be started.
  EXPECT_NE(session_id_, 0);
  EXPECT_TRUE(listener_ != nullptr);
  on_fake_response_sent_closure_ = std::move(on_fake_response_sent);
  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&FakeSpeechRecognitionManager::SetFakeRecognitionResult,
                     base::Unretained(this), end_recognition));
}

void FakeSpeechRecognitionManager::OnRecognitionStarted() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  is_recognizing_ = true;
  // Complete the closure on the UI thread instead of the IO thread to avoid
  // threading issues.
  if (recognition_started_closure_)
    std::move(recognition_started_closure_).Run();
}

void FakeSpeechRecognitionManager::OnRecognitionEnded() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  is_recognizing_ = false;
  // Complete the closure on the UI thread instead of the IO thread to avoid
  // threading issues.
  if (recognition_ended_closure_)
    std::move(recognition_ended_closure_).Run();
}

void FakeSpeechRecognitionManager::OnFakeResponseSent() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (on_fake_response_sent_closure_) {
    std::move(on_fake_response_sent_closure_).Run();
  }
}

void FakeSpeechRecognitionManager::SetFakeResult(const std::string& value,
                                                 bool is_final) {
  fake_result_ = value;
  is_final_ = is_final;
}

int FakeSpeechRecognitionManager::CreateSession(
    const SpeechRecognitionSessionConfig& config) {
  return CreateSession(std::move(config), mojo::NullReceiver(),
                       mojo::NullRemote(), std::nullopt);
}

int FakeSpeechRecognitionManager::CreateSession(
    const SpeechRecognitionSessionConfig& config,
    mojo::PendingReceiver<media::mojom::SpeechRecognitionSession>
        session_receiver,
    mojo::PendingRemote<media::mojom::SpeechRecognitionSessionClient>
        client_remote,
    std::optional<SpeechRecognitionAudioForwarderConfig>
        audio_forwarder_config) {
  VLOG(1) << "FAKE CreateSession invoked.";
  // FakeSpeechRecognitionManager only allows one active session at a time.
  EXPECT_EQ(0, session_id_);
  EXPECT_EQ(nullptr, listener_.get());
  listener_ = config.event_listener.get();
  if (config.grammars.size() > 0)
    grammar_ = config.grammars[0].url.spec();
  session_ctx_ = config.initial_context;
  session_config_ = config;
  session_id_ = 1;
  has_sent_result_ = false;
  return session_id_;
}

void FakeSpeechRecognitionManager::StartSession(int session_id) {
  VLOG(1) << "FAKE StartSession invoked.";
  EXPECT_EQ(session_id, session_id_);
  EXPECT_TRUE(listener_ != nullptr);

  listener_->OnRecognitionStart(session_id_);
  // Delegate can get a copy of events.
  if (delegate_)
    delegate_->GetEventListener()->OnRecognitionStart(session_id_);

  if (should_send_fake_response_) {
    // Give the fake result in a short while.
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            &FakeSpeechRecognitionManager::SetFakeRecognitionResult,
            // This class does not need to be refcounted (typically done by
            // PostTask) since it will outlive the test and gets released only
            // when the test shuts down. Disabling refcounting here saves a bit
            // of unnecessary code and the factory method can return a plain
            // pointer below as required by the real code.
            base::Unretained(this), true /* end recognition */));
  }
  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&FakeSpeechRecognitionManager::OnRecognitionStarted,
                     base::Unretained(this)));
}

void FakeSpeechRecognitionManager::AbortSession(int session_id) {
  VLOG(1) << "FAKE AbortSession invoked.";
  EXPECT_EQ(session_id_, session_id);
  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&FakeSpeechRecognitionManager::OnRecognitionEnded,
                     base::Unretained(this)));
  session_id_ = 0;
  listener_ = nullptr;
}

void FakeSpeechRecognitionManager::StopAudioCaptureForSession(int session_id) {
  VLOG(1) << "StopRecording invoked.";
  EXPECT_EQ(session_id_, session_id);
  // Nothing to do here since we aren't really recording.
}

void FakeSpeechRecognitionManager::AbortAllSessionsForRenderFrame(
    GlobalRenderFrameHostId global_id) {
  VLOG(1) << "CancelAllRequestsWithDelegate invoked.";
  EXPECT_TRUE(should_send_fake_response_ ||
              session_ctx_.global_id == global_id);
  did_cancel_all_ = true;
}

void FakeSpeechRecognitionManager::UpdateRecognitionContextForSession(
    int session_id,
    const media::SpeechRecognitionRecognitionContext& recognition_context) {
  VLOG(1) << "UpdateRecognitionContextForSession invoked.";
  EXPECT_EQ(session_id_, session_id);
}

const SpeechRecognitionSessionConfig&
FakeSpeechRecognitionManager::GetSessionConfig(int session_id) {
  EXPECT_EQ(session_id, session_id_);
  return session_config_;
}

SpeechRecognitionSessionContext FakeSpeechRecognitionManager::GetSessionContext(
    int session_id) {
  EXPECT_EQ(session_id, session_id_);
  return session_ctx_;
}

bool FakeSpeechRecognitionManager::UseOnDeviceSpeechRecognition(
    const SpeechRecognitionSessionConfig& config) {
  return false;
}

void FakeSpeechRecognitionManager::SetFakeRecognitionResult(
    bool end_recognition) {
  if (!session_id_)  // Do a check in case we were cancelled..
    return;
  VLOG(1) << "Setting fake recognition result.";
  if (!has_sent_result_) {
    listener_->OnAudioStart(session_id_);
    listener_->OnSoundStart(session_id_);
    has_sent_result_ = true;
  }
  media::mojom::WebSpeechRecognitionResultPtr result =
      media::mojom::WebSpeechRecognitionResult::New();
  result->hypotheses.push_back(media::mojom::SpeechRecognitionHypothesis::New(
      base::UTF8ToUTF16(fake_result_), 1.0));
  // If `is_provisional` is true, then the result is an interim result that
  // could be changed. Otherwise, it's a final result. Consequently,
  // `is_provisional` is the converse of `is_final`.
  result->is_provisional = !is_final_;
  std::vector<media::mojom::WebSpeechRecognitionResultPtr> results;
  results.push_back(std::move(result));
  listener_->OnRecognitionResults(session_id_, results);
  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&FakeSpeechRecognitionManager::OnFakeResponseSent,
                     base::Unretained(this)));
  if (end_recognition) {
    // End recognition. Note that in normal SpeechRecognitionManager, a session
    // is not ended after the final result is sent. This behavior is just
    // to make testing easier.
    // Check if the listener has destructed itself after a final result.
    if (listener_) {
      listener_->OnAudioEnd(session_id_);
      listener_->OnRecognitionEnd(session_id_);
      GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(&FakeSpeechRecognitionManager::OnRecognitionEnded,
                         base::Unretained(this)));
    }
    session_id_ = 0;
    listener_ = nullptr;
  }
  VLOG(1) << "Finished setting fake recognition result.";
}

void FakeSpeechRecognitionManager::SendFakeError(
    base::OnceClosure on_fake_error_sent) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  EXPECT_NE(session_id_, 0);
  EXPECT_TRUE(listener_ != nullptr);
  on_fake_error_sent_closure_ = std::move(on_fake_error_sent);
  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &FakeSpeechRecognitionManager::SendFakeSpeechRecognitionError,
          base::Unretained(this)));
}

void FakeSpeechRecognitionManager::SendFakeSpeechRecognitionError() {
  if (!session_id_)
    return;

  VLOG(1) << "Sending fake recognition error.";
  listener_->OnRecognitionError(
      session_id_, *media::mojom::SpeechRecognitionError::New(
                       media::mojom::SpeechRecognitionErrorCode::kNetwork,
                       media::mojom::SpeechAudioErrorDetails::kNone));
  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&FakeSpeechRecognitionManager::OnFakeErrorSent,
                                base::Unretained(this)));
  VLOG(1) << "Finished sending fake recognition error.";
}

void FakeSpeechRecognitionManager::OnFakeErrorSent() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (on_fake_error_sent_closure_) {
    std::move(on_fake_error_sent_closure_).Run();
  }
}

}  // namespace content
