// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

#include "pw_bluetooth_sapphire/internal/host/common/error.h"

#include <sstream>

#include "pw_bluetooth_sapphire/internal/host/common/uuid.h"
#include "pw_unit_test/framework.h"

namespace bt {
namespace {

enum class TestError : uint8_t {
  kSuccess = 0,
  kFail1 = 1,
  kFail2 = 2,
};

enum class TestErrorWithoutSuccess {
  kFail0 = 0,
  kFail1 = 1,
};

// Test detail::IsErrorV
static_assert(detail::IsErrorV<Error<TestError>>);
static_assert(!detail::IsErrorV<TestError>);

}  // namespace

// This template specialization must be in ::bt for name lookup reasons
template <>
struct ProtocolErrorTraits<TestError> {
  static std::string ToString(TestError code) {
    switch (code) {
      case TestError::kSuccess:
        return "success (TestError 0)";
      case TestError::kFail1:
        return "fail 1 (TestError 1)";
      case TestError::kFail2:
        return "fail 2 (TestError 2)";
      default:
        return "unknown (TestError)";
    }
  }

  static constexpr bool is_success(TestError proto_code) {
    return proto_code == TestError::kSuccess;
  }
};

template <>
struct ProtocolErrorTraits<TestErrorWithoutSuccess> {
  static std::string ToString(TestErrorWithoutSuccess code) {
    switch (code) {
      case TestErrorWithoutSuccess::kFail0:
        return "fail 0 (TestErrorWithoutSuccess 0)";
      case TestErrorWithoutSuccess::kFail1:
        return "fail 1 (TestErrorWithoutSuccess 1)";
      default:
        return "unknown (TestError)";
    }
  }

  // is_success() is omitted
};

namespace {

// Build an Error when |proto_code| is guaranteed to be an error. This could be
// consteval in C++20 so that if the code isn't an error, it doesn't compile.
constexpr Error<TestError> MakeError(TestError proto_code) {
  return ToResult(proto_code).error_value();
}

TEST(ErrorTest, ResultFromNonSuccessHostError) {
  // Create a result that can hold TestError but which holds a HostError
  constexpr fit::result result = ToResult<TestError>(HostError::kFailed);
  ASSERT_TRUE(result.is_error());

  // Unwrap the result then access Error::is(…)
  constexpr Error error = result.error_value();
  ASSERT_TRUE(error.is_host_error());
  EXPECT_FALSE(error.is_protocol_error());
  EXPECT_EQ(HostError::kFailed, error.host_error());
  EXPECT_TRUE(error.is(HostError::kFailed));
  EXPECT_FALSE(error.is(HostError::kTimedOut));

  // Compare to protocol error
  EXPECT_FALSE(error.is(TestError::kFail1));
  EXPECT_FALSE(error.is(TestError::kSuccess));

  // Compare result to error
  EXPECT_EQ(error, result);
  EXPECT_EQ(result, error);
}

TEST(ErrorTest, ResultFromSuccessHostError) {
  constexpr fit::result result = ToResult<TestError>(TestError::kSuccess);
  ASSERT_EQ(fit::ok(), result);

  // Compare result to error
  const Error error = MakeError(TestError::kFail1);
  EXPECT_NE(error, result);
  EXPECT_NE(result, error);
}

TEST(ErrorTest, ResultFromNonSuccessGeneralHostError) {
  // Create a result that can hold and only holds a HostError
  constexpr fit::result result = ToResult(HostError::kFailed);
  ASSERT_TRUE(result.is_error());

  // Unwrap the result then access Error::is(…)
  constexpr Error general_error = result.error_value();
  ASSERT_TRUE(general_error.is_host_error());
  EXPECT_FALSE(general_error.is_protocol_error());
  EXPECT_EQ(HostError::kFailed, general_error.host_error());
  EXPECT_TRUE(general_error.is(HostError::kFailed));
  EXPECT_FALSE(general_error.is(HostError::kTimedOut));

  // Compare result to error
  EXPECT_EQ(general_error, result);
  EXPECT_EQ(result, general_error);

  // Create a specific kind of Error from the only-HostError-holding Error
  constexpr Error<TestError> specific_error = general_error;
  EXPECT_TRUE(specific_error.is(HostError::kFailed));
  EXPECT_EQ(general_error, specific_error);
  EXPECT_EQ(specific_error, general_error);

  // Test operator!=
  constexpr Error different_specific_error = MakeError(TestError::kFail1);
  EXPECT_NE(general_error, different_specific_error);
  EXPECT_NE(different_specific_error, general_error);
}

TEST(ErrorTest, ResultFromNonSuccessProtocolError) {
  constexpr fit::result result = ToResult(TestError::kFail1);
  ASSERT_TRUE(result.is_error());

  // Unwrap the result then access Error::is(…)
  constexpr Error error = result.error_value();
  ASSERT_TRUE(error.is_protocol_error());
  EXPECT_FALSE(error.is_host_error());
  EXPECT_EQ(TestError::kFail1, error.protocol_error());
  EXPECT_TRUE(error.is(TestError::kFail1));
  EXPECT_FALSE(error.is(TestError::kSuccess));
  EXPECT_FALSE(error.is(TestError::kFail2));

  // Compare to HostError
  EXPECT_FALSE(error.is(HostError::kFailed));

  // Compare result to error
  EXPECT_EQ(error, result);
  EXPECT_EQ(result, error);
}

TEST(ErrorTest, ResultFromSuccessProtocolError) {
  constexpr fit::result result = ToResult(TestError::kSuccess);
  ASSERT_EQ(fit::ok(), result);

  // Compare result to error
  const Error error = MakeError(TestError::kFail1);
  EXPECT_NE(error, result);
  EXPECT_NE(result, error);
}

TEST(ErrorTest, ResultFromNonSuccessProtocolErrorThatOnlyHoldsErrors) {
  // Use public ctor to construct the error directly.
  constexpr Error<TestErrorWithoutSuccess> error(
      TestErrorWithoutSuccess::kFail0);
  EXPECT_TRUE(error.is(TestErrorWithoutSuccess::kFail0));
}

TEST(ErrorDeathTest, ReadingHostErrorThatIsNotPresentIsFatal) {
  const Error error = MakeError(TestError::kFail1);
  ASSERT_DEATH_IF_SUPPORTED([[maybe_unused]] auto _ = error.host_error(),
                            "HostError");
}

TEST(ErrorDeathTest, ReadingProtocolErrorThatIsNotPresentIsFatal) {
  const Error<TestError> error(HostError::kFailed);
  ASSERT_DEATH_IF_SUPPORTED([[maybe_unused]] auto _ = error.protocol_error(),
                            "protocol error");
}

TEST(ErrorTest, ResultIsAnyOf) {
  constexpr Error error = MakeError(TestError::kFail1);

  // None of the arguments compare equal to error's contents
  EXPECT_FALSE(error.is_any_of(
      HostError::kFailed, TestError::kFail2, TestError::kSuccess));

  // One argument matches
  EXPECT_TRUE(error.is_any_of(
      HostError::kFailed, TestError::kFail2, TestError::kFail1));
  EXPECT_TRUE(error.is_any_of(
      HostError::kFailed, TestError::kFail1, TestError::kFail2));
  EXPECT_TRUE(error.is_any_of(TestError::kFail1));
}

TEST(ErrorTest, ErrorCanBeComparedInTests) {
  const Error error = MakeError(TestError::kFail1);

  // Compare to HostError
  EXPECT_FALSE(error.is(HostError::kFailed));

  // Use operator== through GTest
  EXPECT_EQ(error, error);

  // Use operator!= through GTest
  EXPECT_NE(MakeError(TestError::kFail2), error);
  EXPECT_NE(Error<>(HostError::kFailed), error);
  EXPECT_NE(Error<TestError>(HostError::kFailed), error);
}

TEST(ErrorTest, ResultCanBeComparedInTests) {
  constexpr fit::result result = ToResult(TestError::kFail1);

  // Use operator== through GTest
  EXPECT_EQ(result, result);

  // And explicitly
  EXPECT_FALSE(result == ToResult<TestError>(HostError::kCanceled));
  EXPECT_FALSE(result == ToResult(HostError::kCanceled));
  EXPECT_FALSE(result == fit::ok());
  EXPECT_FALSE(result == fit::result<Error<TestError>>(fit::ok()));

  // Use operator!= through GTest
  EXPECT_NE(ToResult<TestError>(HostError::kCanceled), result);
  EXPECT_NE(ToResult(TestError::kFail2), result);

  // Compare to a general result
  EXPECT_NE(ToResult(HostError::kCanceled), result);
  EXPECT_NE(fit::result<Error<NoProtocolError>>(fit::ok()), result);

  // Compare results to fix::success
  EXPECT_NE(fit::ok(), result);
  EXPECT_EQ(fit::ok(), ToResult(TestError::kSuccess));

  const fit::result<Error<TestError>, int> success_with_value = fit::ok(1);
  const fit::result<Error<TestError>, int> error_with_value =
      fit::error(MakeError(TestError::kFail1));
  const fit::result<Error<TestError>, int> different_error_with_value =
      fit::error(MakeError(TestError::kFail2));
  EXPECT_EQ(success_with_value, success_with_value);
  EXPECT_NE(success_with_value, error_with_value);
  EXPECT_FALSE(success_with_value == error_with_value);
  EXPECT_NE(error_with_value, different_error_with_value);

  EXPECT_EQ(ToResult(TestError::kFail1).error_value(), error_with_value);
  EXPECT_NE(ToResult(TestError::kFail2).error_value(), error_with_value);

  const fit::result<Error<TestError>, int> error_with_value_holding_host_error =
      fit::error(Error<TestError>(HostError::kFailed));

  // ToResult(HostError) constructs a bt::Error<NoProtocolError> so comparisons
  // must take this into account.
  EXPECT_EQ(Error(HostError::kFailed), error_with_value_holding_host_error);
  EXPECT_NE(Error(HostError::kFailed), error_with_value);
}

TEST(ErrorTest, VisitOnHostError) {
  constexpr Error<TestError> error(HostError::kFailed);
  ASSERT_TRUE(error.is_host_error());

  bool host_visited = false;
  bool proto_visited = false;
  error.Visit([&host_visited](HostError) { host_visited = true; },
              [&proto_visited](TestError) { proto_visited = true; });
  EXPECT_TRUE(host_visited);
  EXPECT_FALSE(proto_visited);
}

TEST(ErrorTest, VisitOnProtoError) {
  constexpr Error error = MakeError(TestError::kFail1);
  ASSERT_TRUE(error.is_protocol_error());

  bool host_visited = false;
  bool proto_visited = false;
  error.Visit([&host_visited](HostError) { host_visited = true; },
              [&proto_visited](TestError) { proto_visited = true; });
  EXPECT_FALSE(host_visited);
  EXPECT_TRUE(proto_visited);
}

TEST(ErrorTest, HostErrorToString) {
  constexpr Error error(HostError::kFailed);
  EXPECT_EQ(HostErrorToString(error.host_error()), error.ToString());
}

TEST(ErrorTest, GeneralHostErrorToString) {
  constexpr Error error(HostError::kFailed);
  EXPECT_EQ(HostErrorToString(error.host_error()), error.ToString());
}

TEST(ErrorTest, ProtocolErrorToString) {
  constexpr Error error = MakeError(TestError::kFail2);
  EXPECT_EQ(ProtocolErrorTraits<TestError>::ToString(TestError::kFail2),
            error.ToString());

  // Test that GoogleTest's value printer converts to the same string
  EXPECT_EQ(internal::ToString(error), ::testing::PrintToString(error));

  // ostringstream::operator<< returns a ostream&, so test that our operator is
  // compatible
  std::ostringstream oss;
  oss << error;
}

TEST(ErrorTest, ToStringOnResult) {
  constexpr fit::result proto_error_result = ToResult(TestError::kFail2);
  EXPECT_EQ("[result: error(fail 2 (TestError 2))]",
            internal::ToString(proto_error_result));
  constexpr fit::result<Error<TestError>> success_result = fit::ok();
  EXPECT_EQ("[result: ok()]", internal::ToString(success_result));
  constexpr fit::result<Error<TestError>, int> success_result_with_value =
      fit::ok(1);
  EXPECT_EQ("[result: ok(?)]", internal::ToString(success_result_with_value));
  constexpr fit::result<Error<TestError>, UUID>
      success_result_with_printable_value = fit::ok(UUID(uint16_t{}));
  EXPECT_EQ("[result: ok(00000000-0000-1000-8000-00805f9b34fb)]",
            internal::ToString(success_result_with_printable_value));

  // Test that GoogleTest's value printer converts to the same string
  EXPECT_EQ(internal::ToString(proto_error_result),
            ::testing::PrintToString(proto_error_result));
  EXPECT_EQ(internal::ToString(success_result),
            ::testing::PrintToString(success_result));
  EXPECT_EQ(internal::ToString(success_result_with_printable_value),
            ::testing::PrintToString(success_result_with_printable_value));

  // The value printer will try to stream types to the GoogleTest ostream if
  // possible, so it may not always match bt::internal::ToString's output.
  EXPECT_EQ("[result: ok(1)]",
            ::testing::PrintToString(success_result_with_value));
}

TEST(ErrorTest, BtIsErrorMacroCompiles) {
  const fit::result general_error = ToResult(HostError::kFailed);
  EXPECT_TRUE(bt_is_error(general_error, ERROR, "ErrorTest", "error message"));
  const fit::result<Error<TestError>, int> success_with_value = fit::ok(1);
  EXPECT_FALSE(
      bt_is_error(success_with_value, ERROR, "ErrorTest", "error message"));
  const fit::result<Error<TestError>, int> error_with_value =
      fit::error(MakeError(TestError::kFail1));
  EXPECT_TRUE(
      bt_is_error(error_with_value, ERROR, "ErrorTest", "error message"));
}

TEST(ErrorTest, BtIsErrorOnlyEvaluatesResultOnce) {
  int result_count = 0;
  auto result_func = [&]() {
    result_count++;
    return ToResult(TestError::kFail1);
  };
  bt_is_error(result_func(), ERROR, "ErrorTest", "error message");
  EXPECT_EQ(result_count, 1);
}

TEST(ErrorTest, BtStrMacroOnResult) {
  constexpr fit::result proto_error_result = ToResult(TestError::kFail2);
  EXPECT_EQ(internal::ToString(proto_error_result), bt_str(proto_error_result));
}

}  // namespace
}  // namespace bt
