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

#include "chrome/browser/ui/webui/skills/skills_dialog_handler.h"

#include <optional>

#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/types/expected.h"
#include "chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/skills/skills_service_factory.h"
#include "chrome/browser/ui/webui/skills/skills_dialog_delegate.h"
#include "chrome/test/base/testing_profile.h"
#include "components/optimization_guide/core/model_execution/optimization_guide_model_execution_error.h"
#include "components/skills/mocks/mock_skills_service.h"
#include "components/skills/public/skill.mojom.h"
#include "components/skills/public/skills_metrics.h"
#include "components/skills/public/skills_service.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_web_ui.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace skills {
namespace {

using ::testing::_;
using ::testing::AllOf;
using ::testing::Field;
using ::testing::NiceMock;
using ::testing::Optional;

class MockSkillsDialogDelegate : public SkillsDialogDelegate {
 public:
  MOCK_METHOD(void, CloseDialog, (), (override));
  MOCK_METHOD(void, OnSkillSaved, (const std::string&), (override));
  MOCK_METHOD(void, OnSkillDeleted, (const std::string&), (override));
  MOCK_METHOD(BrowserWindowInterface*,
              GetBrowserWindowInterface,
              (),
              (override));

  base::WeakPtr<MockSkillsDialogDelegate> GetWeakPtr() {
    return weak_factory_.GetWeakPtr();
  }

 private:
  base::WeakPtrFactory<MockSkillsDialogDelegate> weak_factory_{this};
};

class TestSkillsDialogHandler : public SkillsDialogHandler {
 public:
  using SkillsDialogHandler::SkillsDialogHandler;

  // Member to hold the saved skill so we can return a valid pointer.
  skills::Skill fake_saved_skill_;

  const skills::Skill* SaveOrUpdateSkill(const skills::Skill& skill) override {
    std::string trimmed_name(
        base::TrimWhitespaceASCII(skill.name, base::TRIM_ALL));
    std::string trimmed_prompt(
        base::TrimWhitespaceASCII(skill.prompt, base::TRIM_ALL));

    if (trimmed_name.empty() || trimmed_prompt.empty()) {
      RecordSkillsSaveResult(SkillsSaveResult::kInvalidRequest);
      return nullptr;
    }

    fake_saved_skill_.id = skill.id.empty() ? "generated_fake_id" : skill.id;
    fake_saved_skill_.name = trimmed_name;
    fake_saved_skill_.icon = skill.icon;
    fake_saved_skill_.prompt = trimmed_prompt;
    return &fake_saved_skill_;
  }
};

class SkillsDialogHandlerTest : public testing::Test {
 public:
  void SetUp() override {
    SkillsServiceFactory::GetInstance()->SetTestingFactory(
        &profile_,
        base::BindLambdaForTesting([](content::BrowserContext* context)
                                       -> std::unique_ptr<KeyedService> {
          return std::make_unique<NiceMock<MockSkillsService>>();
        }));
    mock_dialog_type_ = mojom::SkillsDialogType::kAdd;
    web_contents_ = content::WebContents::Create(
        content::WebContents::CreateParams(&profile_));
    web_ui_.set_web_contents(web_contents_.get());
    handler_ = std::make_unique<TestSkillsDialogHandler>(
        receiver_.BindNewPipeAndPassReceiver(), web_ui_.GetWebContents(),
        &mock_opt_guide_service_, mock_skill_, entrypoint_, mock_dialog_type_,
        mock_delegate_.GetWeakPtr());
  }

  // Helper to mock the Optimization Guide server response
  void SetModelResponse(
      std::optional<std::string> serialized_any_value,
      const std::string& type_url = "type.googleapis.com/SkillsResponse") {
    EXPECT_CALL(mock_opt_guide_service_, ExecuteModel(_, _, _, _))
        .WillOnce([serialized_any_value, type_url](
                      auto capability,
                      const google::protobuf::MessageLite& request,
                      std::optional<optimization_guide::ModelExecutionOptions>
                          options,
                      optimization_guide::
                          OptimizationGuideModelExecutionResultCallback cb) {
          if (!serialized_any_value) {
            auto error = optimization_guide::
                OptimizationGuideModelExecutionError::FromModelExecutionError(
                    optimization_guide::OptimizationGuideModelExecutionError::
                        ModelExecutionError::kGenericFailure);

            std::move(cb).Run(
                optimization_guide::OptimizationGuideModelExecutionResult(
                    base::unexpected(error), nullptr),
                nullptr);
            return;
          }

          // Simulate a successful response with the provided payload
          optimization_guide::proto::Any any;
          any.set_type_url(type_url);
          any.set_value(*serialized_any_value);
          std::move(cb).Run(
              optimization_guide::OptimizationGuideModelExecutionResult(
                  std::move(any), nullptr),
              nullptr);
        });
  }

  // Helper to run the method and assert the metric
  void RunRefineSkillAndExpectError(SkillsDialogHandler* handler,
                                    const std::string& prompt,
                                    skills::SkillsRefineResult expected_error) {
    skills::Skill skill;
    skill.prompt = prompt;
    base::MockCallback<mojom::DialogHandler::RefineSkillCallback> callback;

    handler->RefineSkill(std::move(skill), callback.Get());

    histogram_tester_.ExpectUniqueSample("Skills.Refine.Result", expected_error,
                                         1);
  }

 protected:
  content::BrowserTaskEnvironment task_environment_;
  TestingProfile profile_;
  std::unique_ptr<content::WebContents> web_contents_;
  content::TestWebUI web_ui_;
  MockOptimizationGuideKeyedService mock_opt_guide_service_;
  SkillsDialogEntryPoint entrypoint_ =
      SkillsDialogEntryPoint::kWebClientPrefilled;
  Skill mock_skill_;
  mojom::SkillsDialogType mock_dialog_type_;
  NiceMock<MockSkillsDialogDelegate> mock_delegate_;
  mojo::Remote<mojom::DialogHandler> receiver_;
  std::unique_ptr<TestSkillsDialogHandler> handler_;
  base::HistogramTester histogram_tester_;
};

// Tests that a successful response from MES is correctly mapped to Mojo.
TEST_F(SkillsDialogHandlerTest, RefineSkillSuccess) {
  optimization_guide::proto::SkillsResponse response;
  auto* suggestion = response.add_suggestions();
  suggestion->set_prompt("refined prompt");
  suggestion->set_name("suggested name");
  suggestion->set_icon("🤖");

  SetModelResponse(response.SerializeAsString());

  skills::Skill skill;
  skill.prompt = "test prompt";
  base::MockCallback<mojom::DialogHandler::RefineSkillCallback> callback;

  EXPECT_CALL(
      callback,
      Run(Optional(AllOf(Field(&skills::Skill::prompt, "refined prompt"),
                         Field(&skills::Skill::name, "suggested name"),
                         Field(&skills::Skill::icon, "🤖")))));

  handler_->RefineSkill(std::move(skill), callback.Get());

  histogram_tester_.ExpectUniqueSample("Skills.Refine.Result",
                                       skills::SkillsRefineResult::kSuccess, 1);
  histogram_tester_.ExpectTotalCount("Skills.Refine.Latency", 1);
}

TEST_F(SkillsDialogHandlerTest, RefineSkill_EmptyPrompt_LogsInvalidRequest) {
  RunRefineSkillAndExpectError(handler_.get(), "",
                               skills::SkillsRefineResult::kInvalidRequest);
}

TEST_F(SkillsDialogHandlerTest, RefineSkill_NoService_LogsServiceUnavailable) {
  // Disconnect the pipe from Setup().
  receiver_.reset();
  // Create a handler with a nullptr for Optimization Guide
  auto orphan_handler = std::make_unique<TestSkillsDialogHandler>(
      receiver_.BindNewPipeAndPassReceiver(), web_contents_.get(), nullptr,
      mock_skill_, SkillsDialogEntryPoint::kWebClientPrefilled,
      mock_dialog_type_, mock_delegate_.GetWeakPtr());

  RunRefineSkillAndExpectError(orphan_handler.get(), "test prompt",
                               skills::SkillsRefineResult::kServiceUnavailable);
}

TEST_F(SkillsDialogHandlerTest,
       RefineSkill_ModelFails_LogsModelExecutionFailed) {
  SetModelResponse(std::nullopt);
  RunRefineSkillAndExpectError(
      handler_.get(), "test prompt",
      skills::SkillsRefineResult::kModelExecutionFailed);
  histogram_tester_.ExpectTotalCount("Skills.Refine.Latency", 1);
}

TEST_F(SkillsDialogHandlerTest, RefineSkill_BadProto_LogsParseError) {
  SetModelResponse("garbage data", "type.googleapis.com/WrongType");
  RunRefineSkillAndExpectError(handler_.get(), "test prompt",
                               skills::SkillsRefineResult::kParseError);
  histogram_tester_.ExpectTotalCount("Skills.Refine.Latency", 1);
}

TEST_F(SkillsDialogHandlerTest,
       RefineSkill_EmptySuggestions_LogsNoSuggestions) {
  optimization_guide::proto::SkillsResponse response;  // Valid, but empty
  SetModelResponse(response.SerializeAsString());

  RunRefineSkillAndExpectError(handler_.get(), "test prompt",
                               skills::SkillsRefineResult::kNoSuggestions);
}

TEST_F(SkillsDialogHandlerTest, GenerateNameAndEmojiSuccess) {
  optimization_guide::proto::SkillsResponse response;
  auto* suggestion = response.add_suggestions();
  suggestion->set_prompt("refined prompt");
  suggestion->set_name("suggested name");
  suggestion->set_icon("🤖");

  SetModelResponse(response.SerializeAsString());

  skills::Skill skill;
  skill.prompt = "test prompt";
  base::MockCallback<mojom::DialogHandler::GenerateNameAndEmojiCallback>
      callback;

  EXPECT_CALL(callback,
              Run(Optional(AllOf(Field(&skills::Skill::name, "suggested name"),
                                 Field(&skills::Skill::icon, "🤖")))));

  handler_->GenerateNameAndEmoji(std::move(skill), callback.Get());

  histogram_tester_.ExpectUniqueSample("Skills.Refine.Result",
                                       skills::SkillsRefineResult::kSuccess, 1);
}

TEST_F(SkillsDialogHandlerTest, GenerateNameAndEmoji_SetsTaskType) {
  skills::Skill skill;
  skill.prompt = "test prompt";
  base::MockCallback<mojom::DialogHandler::GenerateNameAndEmojiCallback>
      callback;

  EXPECT_CALL(mock_opt_guide_service_, ExecuteModel(_, _, _, _))
      .WillOnce([](auto capability,
                   const google::protobuf::MessageLite& request,
                   std::optional<optimization_guide::ModelExecutionOptions>
                       options,
                   optimization_guide::
                       OptimizationGuideModelExecutionResultCallback cb) {
        EXPECT_EQ(capability,
                  optimization_guide::ModelBasedCapabilityKey::kSkills);
        auto* skills_request =
            static_cast<const optimization_guide::proto::SkillsRequest*>(
                &request);
        EXPECT_EQ(skills_request->task_type(),
                  optimization_guide::proto::SkillsRequest::GENERATE_METADATA);
        std::move(cb).Run(
            optimization_guide::OptimizationGuideModelExecutionResult(
                base::unexpected(
                    optimization_guide::OptimizationGuideModelExecutionError::
                        FromModelExecutionError(
                            optimization_guide::
                                OptimizationGuideModelExecutionError::
                                    ModelExecutionError::kGenericFailure)),
                nullptr),
            nullptr);
      });

  handler_->GenerateNameAndEmoji(std::move(skill), callback.Get());
}

TEST_F(SkillsDialogHandlerTest,
       GenerateNameAndEmoji_EmptyPrompt_LogsInvalidRequest) {
  skills::Skill skill;
  skill.prompt = "";
  base::MockCallback<mojom::DialogHandler::GenerateNameAndEmojiCallback>
      callback;

  handler_->GenerateNameAndEmoji(std::move(skill), callback.Get());

  histogram_tester_.ExpectUniqueSample(
      "Skills.Refine.Result", skills::SkillsRefineResult::kInvalidRequest, 1);
}

TEST_F(SkillsDialogHandlerTest,
       GenerateNameAndEmoji_NoService_LogsServiceUnavailable) {
  // Disconnect the pipe from Setup().
  receiver_.reset();
  // Create a handler with a nullptr for Optimization Guide
  auto orphan_handler = std::make_unique<TestSkillsDialogHandler>(
      receiver_.BindNewPipeAndPassReceiver(), web_contents_.get(), nullptr,
      mock_skill_, SkillsDialogEntryPoint::kWebClientPrefilled,
      mock_dialog_type_, mock_delegate_.GetWeakPtr());

  skills::Skill skill;
  skill.prompt = "test prompt";
  base::MockCallback<mojom::DialogHandler::GenerateNameAndEmojiCallback>
      callback;

  orphan_handler->GenerateNameAndEmoji(std::move(skill), callback.Get());

  histogram_tester_.ExpectUniqueSample(
      "Skills.Refine.Result", skills::SkillsRefineResult::kServiceUnavailable,
      1);
}

TEST_F(SkillsDialogHandlerTest,
       GenerateNameAndEmoji_ModelFails_LogsModelExecutionFailed) {
  SetModelResponse(std::nullopt);  // Force a total server failure

  skills::Skill skill;
  skill.prompt = "test prompt";
  base::MockCallback<mojom::DialogHandler::GenerateNameAndEmojiCallback>
      callback;

  handler_->GenerateNameAndEmoji(std::move(skill), callback.Get());

  histogram_tester_.ExpectUniqueSample(
      "Skills.Refine.Result", skills::SkillsRefineResult::kModelExecutionFailed,
      1);
}

TEST_F(SkillsDialogHandlerTest, GenerateNameAndEmoji_BadProto_LogsParseError) {
  SetModelResponse("garbage data", "type.googleapis.com/WrongType");

  skills::Skill skill;
  skill.prompt = "test prompt";
  base::MockCallback<mojom::DialogHandler::GenerateNameAndEmojiCallback>
      callback;

  handler_->GenerateNameAndEmoji(std::move(skill), callback.Get());

  histogram_tester_.ExpectUniqueSample(
      "Skills.Refine.Result", skills::SkillsRefineResult::kParseError, 1);
}

TEST_F(SkillsDialogHandlerTest,
       GenerateNameAndEmoji_EmptySuggestions_LogsNoSuggestions) {
  optimization_guide::proto::SkillsResponse response;  // Valid, but empty
  SetModelResponse(response.SerializeAsString());

  skills::Skill skill;
  skill.prompt = "test prompt";
  base::MockCallback<mojom::DialogHandler::GenerateNameAndEmojiCallback>
      callback;

  handler_->GenerateNameAndEmoji(std::move(skill), callback.Get());

  histogram_tester_.ExpectUniqueSample(
      "Skills.Refine.Result", skills::SkillsRefineResult::kNoSuggestions, 1);
}

TEST_F(SkillsDialogHandlerTest, CloseDialog_LogsCancelled) {
  EXPECT_CALL(mock_delegate_, CloseDialog()).Times(1);
  handler_->CloseDialog();

  histogram_tester_.ExpectBucketCount(
      "Skills.Dialog.Creation.WebClient.Prefilled.Action",
      SkillsDialogAction::kCancelled, 1);
}

TEST_F(SkillsDialogHandlerTest, RefineSkill_LogsMetric) {
  skills::Skill skill;
  skill.prompt = "test";
  base::MockCallback<mojom::DialogHandler::RefineSkillCallback> callback;
  EXPECT_CALL(mock_opt_guide_service_, ExecuteModel(_, _, _, _));

  handler_->RefineSkill(std::move(skill), callback.Get());

  histogram_tester_.ExpectBucketCount(
      "Skills.Dialog.Creation.WebClient.Prefilled.Action",
      SkillsDialogAction::kRefined, 1);
}

TEST_F(SkillsDialogHandlerTest, SubmitSkill_LogsSavedAndSuccess) {
  skills::Skill skill;
  skill.name = "Test Skill";
  skill.prompt = "Test Prompt";
  base::MockCallback<mojom::DialogHandler::SubmitSkillCallback> callback;
  EXPECT_CALL(mock_delegate_, OnSkillSaved("generated_fake_id")).Times(1);
  EXPECT_CALL(mock_delegate_, CloseDialog()).Times(1);

  handler_->SubmitSkill(
      skill, skills::mojom::SkillsPromptRefinementOutcome::kNotRefined,
      callback.Get());

  // Metric Check
  histogram_tester_.ExpectBucketCount(
      "Skills.Dialog.Creation.WebClient.Prefilled.Action",
      SkillsDialogAction::kSaved, 1);
  histogram_tester_.ExpectUniqueSample("Skills.Save.Result",
                                       skills::SkillsSaveResult::kSuccess, 1);
}

TEST_F(SkillsDialogHandlerTest, SubmitSkill_LogsUiContextLostWhenDialogClosed) {
  skills::Skill skill;
  skill.name = "Test Skill";
  base::MockCallback<mojom::DialogHandler::SubmitSkillCallback> callback;

  // Disconnect the pipe from the Setup().
  receiver_.reset();
  // Create a new handler but pass an empty WeakPtr for the delegate
  auto orphan_handler = std::make_unique<TestSkillsDialogHandler>(
      receiver_.BindNewPipeAndPassReceiver(), web_contents_.get(),
      &mock_opt_guide_service_, mock_skill_,
      SkillsDialogEntryPoint::kWebClientPrefilled, mock_dialog_type_,
      base::WeakPtr<MockSkillsDialogDelegate>());

  // Attempt to submit the skill.
  orphan_handler->SubmitSkill(
      skill, skills::mojom::SkillsPromptRefinementOutcome::kNotRefined,
      callback.Get());

  // Assert that it safely bailed out and logged the error.
  histogram_tester_.ExpectUniqueSample(
      "Skills.Save.Result", skills::SkillsSaveResult::kUiContextLost, 1);
}

TEST_F(SkillsDialogHandlerTest, SubmitSkill_LogsRefinementOutcome) {
  skills::Skill skill;
  skill.name = "Test Skill";
  skill.prompt = "Test Prompt";
  base::MockCallback<mojom::DialogHandler::SubmitSkillCallback> callback;

  handler_->SubmitSkill(
      skill, skills::mojom::SkillsPromptRefinementOutcome::kUsedRefinedPrompt,
      callback.Get());

  histogram_tester_.ExpectUniqueSample(
      "Skills.Dialog.PromptRefinementOutcome",
      skills::mojom::SkillsPromptRefinementOutcome::kUsedRefinedPrompt, 1);
}

TEST_F(SkillsDialogHandlerTest, SubmitSkill_EmptyName_LogsInvalidRequest) {
  skills::Skill skill;
  skill.name = "   ";
  skill.prompt = "Valid Prompt";
  base::MockCallback<mojom::DialogHandler::SubmitSkillCallback> callback;

  handler_->SubmitSkill(
      skill, skills::mojom::SkillsPromptRefinementOutcome::kNotRefined,
      callback.Get());

  histogram_tester_.ExpectUniqueSample(
      "Skills.Save.Result", skills::SkillsSaveResult::kInvalidRequest, 1);
}

TEST_F(SkillsDialogHandlerTest, SubmitSkill_EmptyPrompt_LogsInvalidRequest) {
  skills::Skill skill;
  skill.name = "Valid Name";
  skill.prompt = "   ";
  base::MockCallback<mojom::DialogHandler::SubmitSkillCallback> callback;

  handler_->SubmitSkill(
      skill, skills::mojom::SkillsPromptRefinementOutcome::kNotRefined,
      callback.Get());

  histogram_tester_.ExpectUniqueSample(
      "Skills.Save.Result", skills::SkillsSaveResult::kInvalidRequest, 1);
}

TEST_F(SkillsDialogHandlerTest, DeleteSkill_LogsDeleted) {
  EXPECT_CALL(mock_delegate_, OnSkillDeleted("test_id")).Times(1);
  EXPECT_CALL(mock_delegate_, CloseDialog()).Times(1);

  handler_->DeleteSkill("test_id");

  histogram_tester_.ExpectBucketCount("Skills.Dialog.Edit.Action",
                                      SkillsDialogAction::kDeleted, 1);
}

// Tests that GetInitialState returns the skill and dialog type passed in during
// construction.
TEST_F(SkillsDialogHandlerTest, GetInitialStateReturnsCorrectData) {
  skills::Skill test_skill;
  test_skill.name = "Unit Test Skill";
  test_skill.prompt = "Unit Test Prompt";
  test_skill.icon = "🧪";
  mojom::SkillsDialogType test_dialog_type = mojom::SkillsDialogType::kEdit;

  // Re-initialize the handler with this data.
  receiver_.reset();
  handler_ = std::make_unique<TestSkillsDialogHandler>(
      receiver_.BindNewPipeAndPassReceiver(), web_contents_.get(),
      &mock_opt_guide_service_, test_skill,
      SkillsDialogEntryPoint::kWebClientPrefilled, test_dialog_type,
      mock_delegate_.GetWeakPtr());

  // Call the API using a lambda as the callback.
  mojom::InitialDialogStatePtr received_state;
  handler_->GetInitialState(base::BindOnce(
      [](mojom::InitialDialogStatePtr* out_state,
         mojom::InitialDialogStatePtr result) {
        *out_state = std::move(result);
      },
      &received_state));

  EXPECT_EQ(received_state->dialog_type, test_dialog_type);
  EXPECT_EQ(received_state->skill.name, test_skill.name);
  EXPECT_EQ(received_state->skill.prompt, test_skill.prompt);
  EXPECT_EQ(received_state->skill.icon, test_skill.icon);
}

}  // namespace
}  // namespace skills
