#include <balancer/kernel/dns/async_cache.h>
#include <balancer/kernel/dns/async_coro_i.h>
#include <balancer/kernel/dns/async_entry.h>
#include <balancer/kernel/dns/common.h>
#include <balancer/kernel/dns/resolver_face.h>
#include <balancer/kernel/net/addr.h>

#include <library/cpp/coroutine/engine/impl.h>

#include <library/cpp/testing/gmock_in_unittest/gmock.h>
#include <library/cpp/testing/unittest/registar.h>

#include <chrono>
#include <thread>


using namespace ::testing;

namespace NSrvKernel::NDns::NTests {

namespace {
constexpr uint64_t TestTimeout = 2;
constexpr uint64_t TestTimeoutExpired = TestTimeout * 3 / 2;
}

class TAsyncCoroMock : public IAsyncCoro {
public:
    MOCK_METHOD(bool, Resolve, (const TString&, int, TInstant, IEntry&), (override, noexcept));
};

class TCountersMock : public ICounters {
public:
    MOCK_METHOD(void, AddResolve, (), (override, noexcept));
    MOCK_METHOD(void, AddNoData, (), (override, noexcept));
    MOCK_METHOD(void, AddNxdomain, (), (override, noexcept));
    MOCK_METHOD(void, AddResolveError, (), (override, noexcept));
    MOCK_METHOD(void, AddCacheResolve, (), (override, noexcept));
    MOCK_METHOD(void, AddCacheError, (), (override, noexcept));
};

class TEntryBuilderMock : public IEntryBuilder {
public:
    MOCK_METHOD(TEntryHolder, GetNewEntry, (TContExecutor* e, TDuration ttl), (const, override));
};

class TEntryFake : public TEntry {
public:
    TEntryFake(TContExecutor* e, TDuration resolveTtl) : TEntry(e, resolveTtl) {}

    void SetWaitDResult(int result) { Result_ = result; }

    bool WaitD(TInstant /*deadline*/, int& waitResultCode) noexcept override
    {
        waitResultCode = Result_;
        return waitResultCode == EWAKEDUP;
    }

private:
    int Result_ = EWAKEDUP; // success by default
};

TSockAddr ExtractFirst(TErrorOr<TSockAddrInfo>& errorOrResult)
{
    TSockAddrInfo sockAddr;
    auto error = errorOrResult.AssignTo(sockAddr);
    UNIT_ASSERT(!error);
    UNIT_ASSERT(sockAddr.Addresses.size() == 1);
    return sockAddr.Addresses[0];
}

class TCacheFixture : public NUnitTest::TBaseFixture {
public:
    TCacheFixture()
        : Executor_(MakeHolder<TContExecutor>(64 * 1024))
        , Config_(TDuration::Seconds(3), TDuration::Seconds(180),
                  TDuration::Seconds(3), "", 0, "/tmp/TCacheFixture", TDuration::Seconds(1), true)
    {
        RecreateCache(Config_);
        MakeResult(Host1Info_, Host1Result_);
        MakeResult(Host2Info_, Host2Result_);
        MakeResult(Host3Info_, Host3Result_);
        MakeSeveralIpResult();
    }

    void RecreateCache(const TResolverConfig& config)
    {
        auto resolver = MakeHolder<TAsyncCoroMock>();
        ResolverMock_ = resolver.Get();
        auto builder = MakeHolder<TEntryBuilderMock>();
        EntryBuilderMock_ = builder.Get();
        Cache_ = MakeHolder<TAsyncCache>(Executor_.Get(), config, std::move(resolver), std::move(builder), CountersMock_);
    }

private:
    void MakeResult(const TResolveInfo& /*hostInfo*/, TSockAddr& result)
    {
        static uint16_t lastOctet = 1;
        const TString ip = "192.99.99." + ToString(lastOctet++);
        auto errorOrResult = TSockAddr::FromIpPort(ip, 0); // returned port should be taken from info
        auto err = errorOrResult.AssignTo(result);
        Y_ASSERT(!err);
    }
    void MakeSeveralIpResult()
    {
        auto errorOrResult = TSockAddr::FromIpPort("192.99.99.1", 0);
        auto err = errorOrResult.AssignTo(Host4Result1V4_);
        Y_ASSERT(!err);

        errorOrResult = TSockAddr::FromIpPort("2a02:6b8::2:242", 0);
        err = errorOrResult.AssignTo(Host4Result2V6_);
        Y_ASSERT(!err);

        errorOrResult = TSockAddr::FromIpPort("87.250.250.242", 0);
        err = errorOrResult.AssignTo(Host4Result3V4_);
        Y_ASSERT(!err);

        errorOrResult = TSockAddr::FromIpPort("::1", 0);
        err = errorOrResult.AssignTo(Host4Result4V6_);
        Y_ASSERT(!err);
    }

protected:
    THolder<TContExecutor> Executor_;
    TResolverConfig        Config_;
    TAsyncCoroMock*        ResolverMock_ = nullptr;
    TEntryFake*            EntryFake_ = nullptr;
    TEntryBuilderMock*     EntryBuilderMock_ = nullptr;
    TCountersMock          CountersMock_;
    THolder<IAsyncCache>   Cache_;
    TResolveInfo           Host1Info_{"test.host1.ru", AF_UNSPEC, 80, TResolveInfo::GENERAL};
    TSockAddr              Host1Result_;
    TResolveInfo           Host2Info_{"test.host2.ru", AF_UNSPEC, 81, TResolveInfo::GENERAL};
    TSockAddr              Host2Result_;
    TResolveInfo           Host3Info_{"test.host3.ru", AF_UNSPEC, 82, TResolveInfo::GENERAL};
    TSockAddr              Host3Result_;
    TResolveInfo           Host4Info_{"several-ip.host3.ru", AF_UNSPEC, 443, TResolveInfo::GENERAL};
    TSockAddr              Host4Result1V4_;
    TSockAddr              Host4Result2V6_;
    TSockAddr              Host4Result3V4_;
    TSockAddr              Host4Result4V6_;
};


Y_UNIT_TEST_SUITE_F(DnsAsyncCache, TCacheFixture)
{

Y_UNIT_TEST_F(CacheOnlyCall_DeadlineIsNow, TCacheFixture)
{
    UNIT_ASSERT(ResolverMock_ && EntryBuilderMock_);
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1).WillRepeatedly(
            Invoke([&](const TString&, int, TInstant deadline, IEntry& result) {
                EXPECT_LE(deadline, TInstant::Now());
                TSockAddr addr = Host1Result_;
                result.AddResult(std::move(addr));
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, true, TInstant::Now() + TDuration::Hours(1));
    EXPECT_FALSE(errorOrResult);
}

Y_UNIT_TEST_F(ResolveCall_OriginalDeadline, TCacheFixture)
{
    UNIT_ASSERT(ResolverMock_);
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1).WillRepeatedly(
            Invoke([&](const TString&, int, TInstant deadline, IEntry& result) {
                EXPECT_GT(deadline, TInstant::Now());
                TSockAddr addr = Host1Result_;
                result.AddResult(std::move(addr));
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now() + TDuration::Hours(1));
    EXPECT_FALSE(errorOrResult);
}

Y_UNIT_TEST_F(EmptyCache_ResolverCalled, TCacheFixture)
{
    UNIT_ASSERT(ResolverMock_);
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1).WillRepeatedly(
            Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddResult(std::move(addr));
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
}

Y_UNIT_TEST_F(CallCacheOnlyResolvedHostSeveralTimes_ResolverCalledOnce, TCacheFixture)
{
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1).WillRepeatedly(
            Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddResult(std::move(addr));
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(2);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, true, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    errorOrResult = Cache_->LookupOrResolve(Host1Info_, true, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
}

Y_UNIT_TEST_F(CallResolvedHostSeveralTimes_ResolverCalledOnce, TCacheFixture)
{
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1).WillRepeatedly(
            Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddResult(std::move(addr));
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(2);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    errorOrResult = Cache_->LookupOrResolve(Host1Info_, true, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
}

Y_UNIT_TEST_F(CallCacheOnlyNotResolvedHostSeveralTimes_InProgress_ResolverCalledOnce, TCacheFixture)
{
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                auto entry = MakeHolder<TEntryFake>(e, ttl);
                EntryFake_ = entry.Get();
                return entry;
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1).WillOnce(
            Invoke([&](const TString&, int, TInstant, IEntry& result) {
                result.AddRequestInProgress();
                return false;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(3);

    auto errorOrResult = Cache_->LookupOrResolve(Host2Info_, true, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
    EntryFake_->SetWaitDResult(ETIMEDOUT);
    errorOrResult = Cache_->LookupOrResolve(Host2Info_, true, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
    errorOrResult = Cache_->LookupOrResolve(Host2Info_, false, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
    EntryFake_->SetWaitDResult(ECANCELED);
    errorOrResult = Cache_->LookupOrResolve(Host2Info_, false, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
}

Y_UNIT_TEST_F(CallNotResolvedHostSeveralTimes_ResolveError_ResolverCalledOnce, TCacheFixture)
{
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1).WillOnce(
            Invoke([&](const TString&, int, TInstant, IEntry& result) {
                result.AddRequestInProgress();
                result.SetFailedIfDone();
                CountersMock_.AddResolveError();
                return false;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(1);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(3);

    auto errorOrResult = Cache_->LookupOrResolve(Host2Info_, false, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
    errorOrResult = Cache_->LookupOrResolve(Host2Info_, true, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
    errorOrResult = Cache_->LookupOrResolve(Host2Info_, true, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
    errorOrResult = Cache_->LookupOrResolve(Host2Info_, false, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
}

Y_UNIT_TEST_F(TwoHostsResolve_ResolverCalledTwiceAndPortsAreCorrect, TCacheFixture)
{
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(2).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(2)
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddResult(std::move(addr));
                CountersMock_.AddResolve();
                return true;
            }))
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host2Result_;
                result.AddResult(std::move(addr));
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(2);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, true, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    EXPECT_EQ(Host1Info_.Port, ExtractFirst(errorOrResult).Port());

    errorOrResult = Cache_->LookupOrResolve(Host2Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    EXPECT_EQ(Host2Info_.Port, ExtractFirst(errorOrResult).Port());
}

Y_UNIT_TEST_F(SameIpDifferentPorts_ResolverCalledOnce, TCacheFixture)
{
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1).WillRepeatedly(
            Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddResult(std::move(addr));
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    EXPECT_EQ(Host1Info_.Port, ExtractFirst(errorOrResult).Port());

    Host1Info_.Port++;

    errorOrResult = Cache_->LookupOrResolve(Host1Info_, true, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
}

Y_UNIT_TEST_F(CallFailedToResolveAfterFailTimeout_ResolverCalledTwice, TCacheFixture)
{
    Config_.ErrorTtl = TDuration::Seconds(TestTimeout);
    RecreateCache(Config_);

    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );
    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(2)
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                result.AddRequestInProgress();
                result.SetFailedIfDone();
                CountersMock_.AddResolveError();
                return false;
            }))
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddRequestInProgress();
                result.AddResult(std::move(addr));
                result.SetFailedIfDone();
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(1);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(1);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_TRUE(errorOrResult);
    // While failed resolve TTL is not passed, error from cache is returned
    errorOrResult = Cache_->LookupOrResolve(Host1Info_, true, TInstant::Now());
    EXPECT_TRUE(errorOrResult);

    std::this_thread::sleep_for(std::chrono::seconds(TestTimeoutExpired));

    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    EXPECT_EQ(Host1Info_.Port, ExtractFirst(errorOrResult).Port());

    errorOrResult = Cache_->LookupOrResolve(Host1Info_, true, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
}

Y_UNIT_TEST_F(RecordTtlExpired_ResolverCalledAgain, TCacheFixture)
{
    Config_.RecordTtl = TDuration::Seconds(TestTimeout);
    RecreateCache(Config_);

    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );

    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(2)
            .WillRepeatedly(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddRequestInProgress();
                result.AddResult(std::move(addr));
                result.SetFailedIfDone();
                CountersMock_.AddResolve();
                return true;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(2);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    auto result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());

    Host1Info_.Port++;

    // While TTL is not passed, result from cache is returned, port set from request
    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());

    std::this_thread::sleep_for(std::chrono::seconds(TestTimeoutExpired));

    Host1Info_.Port++;

    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());
}

Y_UNIT_TEST_F(RecordTtlExpired_SecondResolveInProcess_ExpiredCacheIsUsed, TCacheFixture)
{
    Config_.RecordTtl = TDuration::Seconds(TestTimeout);
    RecreateCache(Config_);

    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );

    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(2)
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddRequestInProgress();
                result.AddRequestInProgress(); // test AF_UNSPEC case
                CountersMock_.AddResolveError();
                result.SetFailedIfDone(); // first request failed
                result.AddResult(std::move(addr));
                result.SetFailedIfDone();
                CountersMock_.AddResolve();
                return true;
            }))
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                result.AddRequestInProgress();
                return false;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(1);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(2);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    auto result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());

    Host1Info_.Port++;

    // While TTL is not passed, result from cache is returned, port set from request
    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());

    std::this_thread::sleep_for(std::chrono::seconds(TestTimeoutExpired));

    Host1Info_.Port++;

    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());
}

Y_UNIT_TEST_F(RecordTtlExpired_SecondResolveFailed_ExpiredCacheIsUsed, TCacheFixture)
{
    Config_.RecordTtl = TDuration::Seconds(TestTimeout);
    RecreateCache(Config_);

    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );

    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(2)
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddRequestInProgress();
                result.AddRequestInProgress(); // test AF_UNSPEC case
                result.AddResult(std::move(addr)); // first request succeeded
                result.SetFailedIfDone();
                CountersMock_.AddNoData();
                result.SetFailedIfDone();
                CountersMock_.AddResolve();
                return true;
            }))
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                result.AddRequestInProgress();
                result.SetFailedIfDone();
                return false;
            })
    );

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(1);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(2);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    auto result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());

    Host1Info_.Port++;

    // While TTL is not passed, result from cache is returned, port set from request
    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());

    std::this_thread::sleep_for(std::chrono::seconds(TestTimeoutExpired));

    Host1Info_.Port++;

    errorOrResult = Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    result = ExtractFirst(errorOrResult);
    EXPECT_EQ(Host1Result_.Ip(), result.Ip());
    EXPECT_EQ(Host1Info_.Port, result.Port());
}

Y_UNIT_TEST_F(SeveralSimultaneousRequestsSameHostName_ResolverCalledOnce, TCacheFixture)
{
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );

    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1)
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host1Result_;
                result.AddResult(std::move(addr));
                std::this_thread::sleep_for(std::chrono::seconds(TestTimeout));
                CountersMock_.AddResolve();
                return true;
            }));

    constexpr uint64_t numberOfRequests = 255;

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(numberOfRequests - 1);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    std::vector<TResolveInfo> hostRequests;
    std::vector<TErrorOr<TSockAddrInfo>> results;
    for (uint16_t inc = 1; inc < numberOfRequests; ++inc) {
        hostRequests.push_back({Host1Info_.Host, AF_UNSPEC, static_cast<uint16_t>(Host1Info_.Port + inc), Host1Info_.Type});
    }
    // Make some ports equal to cover more test branches
    UNIT_ASSERT(hostRequests.size() > 3);
    hostRequests[0].Port = hostRequests[1].Port;
    hostRequests[2].Port = hostRequests[3].Port;

    for (uint64_t i = 0; i < numberOfRequests; ++i) {
        results.push_back(Cache_->LookupOrResolve(Host1Info_, false, TInstant::Now()));
    }

    for (uint64_t i = 0; i < numberOfRequests; ++i) {
        EXPECT_FALSE(results[i]);
        EXPECT_EQ(Host1Info_.Port, ExtractFirst(results[i]).Port());
    }
}

Y_UNIT_TEST_F(IpV4AndIpV6Mixed_ResultSortedIpV6First, TCacheFixture)
{
    EXPECT_CALL(*(EntryBuilderMock_), GetNewEntry(_, _)).Times(1).WillRepeatedly(
            Invoke([&](TContExecutor* e, TDuration ttl) {
                return MakeHolder<TEntryFake>(e, ttl);
            })
    );

    EXPECT_CALL(*(ResolverMock_), Resolve(_, _, _, _)).Times(1)
            .WillOnce(Invoke([&](const TString&, int, TInstant, IEntry& result) {
                TSockAddr addr = Host4Result1V4_;
                result.AddResult(std::move(addr));
                addr = Host4Result2V6_;
                result.AddResult(std::move(addr));
                addr = Host4Result3V4_;
                result.AddResult(std::move(addr));
                addr = Host4Result4V6_;
                result.AddResult(std::move(addr));

                std::this_thread::sleep_for(std::chrono::seconds(TestTimeout));
                CountersMock_.AddResolve();
                return true;
            }));

    EXPECT_CALL(CountersMock_, AddResolve()).Times(1);
    EXPECT_CALL(CountersMock_, AddNoData()).Times(0);
    EXPECT_CALL(CountersMock_, AddNxdomain()).Times(0);
    EXPECT_CALL(CountersMock_, AddResolveError()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheResolve()).Times(0);
    EXPECT_CALL(CountersMock_, AddCacheError()).Times(0);

    auto errorOrResult = Cache_->LookupOrResolve(Host4Info_, false, TInstant::Now());
    EXPECT_FALSE(errorOrResult);
    TSockAddrInfo sockAddr;
    auto error = errorOrResult.AssignTo(sockAddr);
    UNIT_ASSERT(!error);
    UNIT_ASSERT(sockAddr.Addresses.size() == 4);

    EXPECT_EQ(Host4Result2V6_.Ip(), sockAddr.Addresses[0].Ip());
    EXPECT_EQ(Host4Info_.Port, sockAddr.Addresses[0].Port());

    EXPECT_EQ(Host4Result4V6_.Ip(), sockAddr.Addresses[1].Ip());
    EXPECT_EQ(Host4Info_.Port, sockAddr.Addresses[1].Port());

    EXPECT_EQ(Host4Result1V4_.Ip(), sockAddr.Addresses[2].Ip());
    EXPECT_EQ(Host4Info_.Port, sockAddr.Addresses[2].Port());

    EXPECT_EQ(Host4Result3V4_.Ip(), sockAddr.Addresses[3].Ip());
    EXPECT_EQ(Host4Info_.Port, sockAddr.Addresses[3].Port());
}

} // ens of fixture pack

}