#include <dht/tx.hpp>
#include <service/tag.hpp>
#include <test_util.hpp>

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

using namespace llarp;
using namespace ::testing;

using llarp::test::makeBuf;

using Val_t = llarp::service::Tag;

// Mock implementation of TX.
struct TestTx final : public dht::TX< dht::Key_t, Val_t >
{
  TestTx(const dht::TXOwner& asker, const dht::Key_t& k,
         dht::AbstractContext* p)
      : dht::TX< dht::Key_t, Val_t >(asker, k, p)
  {
  }

  MOCK_CONST_METHOD1(Validate, bool(const Val_t&));

  MOCK_METHOD1(Start, void(const dht::TXOwner&));

  MOCK_METHOD2(GetNextPeer, bool(dht::Key_t&, const std::set< dht::Key_t >&));

  MOCK_METHOD1(DoNextRequest, void(const dht::Key_t&));

  MOCK_METHOD0(SendReply, void());
};

struct TestDhtTx : public Test
{
  dht::TXOwner asker;
  dht::Key_t key;
  TestTx tx;

  TestDhtTx() : tx(asker, key, nullptr)
  {
  }
};

TEST_F(TestDhtTx, on_found)
{
  // Concerns
  // - Validate returns true
  // - Repeated call on success
  // - Validate returns false
  // - Repeated call on failure
  // - Repeated call on success after failure

  const auto key = makeBuf< dht::Key_t >(0x00);
  Val_t val("good value");

  // Validate returns true
  {
    EXPECT_CALL(tx, Validate(val)).WillOnce(Return(true));

    tx.OnFound(key, val);

    ASSERT_THAT(tx.peersAsked, Contains(key));
    ASSERT_THAT(tx.valuesFound, Contains(val));
  }

  // Repeated call on success
  {
    EXPECT_CALL(tx, Validate(val)).WillOnce(Return(true));
    tx.OnFound(key, val);
    ASSERT_THAT(tx.peersAsked, Contains(key));
    ASSERT_THAT(tx.valuesFound, Contains(val));
  }

  const auto key1 = makeBuf< dht::Key_t >(0x01);
  Val_t badVal("bad value");

  // Validate returns false
  {
    EXPECT_CALL(tx, Validate(badVal)).WillOnce(Return(false));

    tx.OnFound(key1, badVal);

    ASSERT_THAT(tx.peersAsked, Contains(key1));
    ASSERT_THAT(tx.valuesFound, Not(Contains(badVal)));
  }

  // Repeated call on failure
  {
    EXPECT_CALL(tx, Validate(badVal)).WillOnce(Return(false));

    tx.OnFound(key1, badVal);

    ASSERT_THAT(tx.peersAsked, Contains(key1));
    ASSERT_THAT(tx.valuesFound, Not(Contains(badVal)));
  }

  // Repeated call on success after failure
  {
    EXPECT_CALL(tx, Validate(badVal)).WillOnce(Return(true));

    tx.OnFound(key1, badVal);

    ASSERT_THAT(tx.peersAsked, Contains(key1));
    ASSERT_THAT(tx.valuesFound, Contains(badVal));
  }
}

TEST_F(TestDhtTx, ask_next_peer)
{
  // Concerns:
  // - GetNextPeer fails
  // - Next Peer is not closer
  // - next ptr is null
  // - next ptr is not null

  const auto key0 = makeBuf< dht::Key_t >(0x00);
  const auto key1 = makeBuf< dht::Key_t >(0x01);
  const auto key2 = makeBuf< dht::Key_t >(0x02);
  {
    // GetNextPeer fails
    EXPECT_CALL(tx, GetNextPeer(_, _)).WillOnce(Return(false));

    EXPECT_CALL(tx, DoNextRequest(key1)).Times(0);

    ASSERT_FALSE(tx.AskNextPeer(key0, {}));
    ASSERT_THAT(tx.peersAsked, Contains(key0));

    tx.peersAsked.clear();
  }

  {
    // Next Peer is not closer
    EXPECT_CALL(tx, GetNextPeer(_, _))
        .WillOnce(DoAll(SetArgReferee< 0 >(key1), Return(true)));

    EXPECT_CALL(tx, DoNextRequest(key1)).Times(0);

    ASSERT_FALSE(tx.AskNextPeer(key0, {}));
    ASSERT_THAT(tx.peersAsked, Contains(key0));

    tx.peersAsked.clear();
  }

  {
    // next ptr is null
    EXPECT_CALL(tx, GetNextPeer(_, _))
        .WillOnce(DoAll(SetArgReferee< 0 >(key1), Return(true)));

    EXPECT_CALL(tx, DoNextRequest(key1)).Times(1);

    ASSERT_TRUE(tx.AskNextPeer(key2, {}));
    ASSERT_THAT(tx.peersAsked, Contains(key2));

    tx.peersAsked.clear();
  }

  {
    // next ptr is not null
    EXPECT_CALL(tx, GetNextPeer(_, _)).Times(0);

    EXPECT_CALL(tx, DoNextRequest(key1)).Times(1);

    auto ptr = std::make_unique< dht::Key_t >(key1);
    ASSERT_TRUE(tx.AskNextPeer(key2, ptr));
    ASSERT_THAT(tx.peersAsked, Contains(key2));

    tx.peersAsked.clear();
  }
}
