// 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/gap/bredr_connection_request.h"

#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <pw_async/fake_dispatcher_fixture.h>

#include "pw_bluetooth_sapphire/internal/host/common/device_address.h"
#include "pw_bluetooth_sapphire/internal/host/hci-spec/constants.h"
#include "pw_bluetooth_sapphire/internal/host/testing/inspect.h"

namespace bt::gap {
namespace {

using namespace inspect::testing;

const DeviceAddress kTestAddr(DeviceAddress::Type::kBREDR, {1});
const PeerId kPeerId;
constexpr hci::Error RetryableError =
    ToResult(pw::bluetooth::emboss::StatusCode::PAGE_TIMEOUT).error_value();

using BrEdrConnectionRequestTests = pw::async::test::FakeDispatcherFixture;

TEST_F(BrEdrConnectionRequestTests, IncomingRequestStatusTracked) {
  // A freshly created request is not yet incoming
  auto req = BrEdrConnectionRequest(dispatcher(),
                                    kTestAddr,
                                    kPeerId,
                                    Peer::InitializingConnectionToken([] {}));
  EXPECT_FALSE(req.HasIncoming());

  req.BeginIncoming();
  // We should now have an incoming request, but still not an outgoing
  EXPECT_TRUE(req.HasIncoming());
  EXPECT_FALSE(req.AwaitingOutgoing());

  // A completed request is no longer incoming
  req.CompleteIncoming();
  EXPECT_FALSE(req.HasIncoming());
}

TEST_F(BrEdrConnectionRequestTests, CallbacksExecuted) {
  bool callback_called = false;
  bool token_destroyed = false;
  auto req = BrEdrConnectionRequest(
      dispatcher(),
      kTestAddr,
      kPeerId,
      Peer::InitializingConnectionToken(
          [&token_destroyed] { token_destroyed = true; }),
      [&callback_called](auto, auto) { callback_called = true; });

  // A freshly created request with a callback is awaiting outgoing
  EXPECT_TRUE(req.AwaitingOutgoing());
  // Notifying callbacks triggers the callback
  req.NotifyCallbacks(fit::ok(), [&]() {
    EXPECT_TRUE(token_destroyed);
    return nullptr;
  });
  EXPECT_TRUE(token_destroyed);
  EXPECT_TRUE(callback_called);
}

#ifndef NINSPECT
TEST_F(BrEdrConnectionRequestTests, Inspect) {
  // inspector must outlive request
  inspect::Inspector inspector;
  BrEdrConnectionRequest req(dispatcher(),
                             kTestAddr,
                             kPeerId,
                             Peer::InitializingConnectionToken([] {}),
                             [](auto, auto) {});
  req.BeginIncoming();
  req.AttachInspect(inspector.GetRoot(), "request_name");

  auto hierarchy = inspect::ReadFromVmo(inspector.DuplicateVmo());
  EXPECT_THAT(
      hierarchy.value(),
      ChildrenMatch(ElementsAre(NodeMatches(AllOf(
          NameMatches("request_name"),
          PropertyList(UnorderedElementsAre(
              StringIs("peer_id", kPeerId.ToString()),
              UintIs("callbacks", 1u),
              BoolIs("has_incoming", true),
              IntIs("first_create_connection_request_timestamp", -1))))))));
}
#endif  // NINSPECT

class BrEdrConnectionRequestLoopTest
    : public pw::async::test::FakeDispatcherFixture {
 protected:
  using OnComplete = BrEdrConnectionRequest::OnComplete;

  BrEdrConnectionRequestLoopTest()
      : req_(dispatcher(),
             kTestAddr,
             kPeerId,
             Peer::InitializingConnectionToken([] {}),
             [this](hci::Result<> res, BrEdrConnection* conn) {
               if (handler_) {
                 handler_(res, conn);
               }
             }) {
    // By default, an outbound ConnectionRequest with a complete handler that
    // just logs the result.
    handler_ = [](hci::Result<> res, auto /*ignore*/) {
      bt_log(INFO,
             "gap-bredr-test",
             "outbound connection request complete: %s",
             bt_str(res));
    };
  }

  void set_on_complete(BrEdrConnectionRequest::OnComplete handler) {
    handler_ = std::move(handler);
  }

  BrEdrConnectionRequest& connection_req() { return req_; }

 private:
  BrEdrConnectionRequest req_;
  OnComplete handler_;
};
using BrEdrConnectionRequestLoopDeathTest = BrEdrConnectionRequestLoopTest;

TEST_F(BrEdrConnectionRequestLoopTest,
       RetryableErrorCodeShouldRetryAfterFirstCreateConnection) {
  connection_req().RecordHciCreateConnectionAttempt();
  RunFor(std::chrono::seconds(1));
  EXPECT_TRUE(connection_req().ShouldRetry(RetryableError));
}

TEST_F(BrEdrConnectionRequestLoopTest,
       ShouldntRetryBeforeFirstCreateConnection) {
  EXPECT_FALSE(connection_req().ShouldRetry(RetryableError));
}

TEST_F(BrEdrConnectionRequestLoopTest, ShouldntRetryWithNonRetriableErrorCode) {
  connection_req().RecordHciCreateConnectionAttempt();
  RunFor(std::chrono::seconds(1));
  EXPECT_FALSE(connection_req().ShouldRetry(hci::Error(HostError::kCanceled)));
}

TEST_F(BrEdrConnectionRequestLoopTest, ShouldntRetryAfterThirtySeconds) {
  connection_req().RecordHciCreateConnectionAttempt();
  RunFor(std::chrono::seconds(15));
  // Should be OK to retry after 15 seconds
  EXPECT_TRUE(connection_req().ShouldRetry(RetryableError));
  connection_req().RecordHciCreateConnectionAttempt();

  // Should still be OK to retry, even though we've already retried
  RunFor(std::chrono::seconds(14));
  EXPECT_TRUE(connection_req().ShouldRetry(RetryableError));
  connection_req().RecordHciCreateConnectionAttempt();

  RunFor(std::chrono::seconds(1));
  EXPECT_FALSE(connection_req().ShouldRetry(RetryableError));
}

}  // namespace
}  // namespace bt::gap
