#include "dirut.h"
#include "iterator.h"

#include <library/cpp/unittest/registar.h>

#include <util/system/fs.h>
#include <util/system/file.h>
#include <util/generic/hash.h>
#include <util/generic/algorithm.h>
#include <util/random/mersenne.h>

static TString JoinWithNewline(const TVector<TString>& strings) {
    TStringStream ss;
    for (const auto& string : strings) {
        ss << string << "\n";
    }
    return ss.Str();
}

class TDirIteratorTest: public TTestBase {
    UNIT_TEST_SUITE(TDirIteratorTest);
    UNIT_TEST(TestIt)
    UNIT_TEST(TestError)
    UNIT_TEST(TestLocal)
    UNIT_TEST(TestSkip)
    UNIT_TEST(TestSort)
    UNIT_TEST_SUITE_END();

private:
    class TDirHier {
    public:
        struct TPath {
            TString Path;
            int Type;
        };

        inline void AddFile(const TString& path) {
            Add(path, 0);
        }

        inline void AddDir(const TString& path) {
            Add(path, 1);
        }

        inline void Add(const TString& path, int type) {
            const TPath p = {
                path, type};

            Add(p);
        }

        inline void Add(const TPath& path) {
            switch (path.Type) {
                case 0:
                    TFile(path.Path, CreateAlways | RdWr);
                    break;

                case 1:
                    MakeDirIfNotExist(path.Path.data());
                    break;

                case 2:
                    ythrow yexception() << "unknown path type";
            }

            Paths_.push_back(path);
            Srch_[path.Path] = path;
        }

        inline int Type(const TString& path) {
            THashMap<TString, TPath>::const_iterator it = Srch_.find(path);

            UNIT_ASSERT(it != Srch_.end());

            return it->second.Type;
        }

        inline bool Have(const TString& path, int type) {
            return Type(path) == type;
        }

        inline ~TDirHier() {
            for (size_t i = 0; i < Paths_.size(); ++i) {
                NFs::Remove(Paths_[Paths_.size() - i - 1].Path);
            }
        }

    private:
        TVector<TPath> Paths_;
        THashMap<TString, TPath> Srch_;
    };

    inline void TestLocal() {
        TString dirname("." LOCSLASH_S);
        TDirIterator d(dirname, FTS_NOCHDIR);
        for (auto it = d.begin(); it != d.end(); ++it) {
        }
    }

    inline void TestIt() {
        TDirHier hier;

        const TString dir = "tmpdir";
        const TDirHier::TPath path = {dir, 1};

        hier.Add(path);

        for (size_t i = 0; i < 10; ++i) {
            const TString dir1 = dir + LOCSLASH_C + ToString(i);
            const TDirHier::TPath path1 = {dir1, 1};

            hier.Add(path1);

            for (size_t j = 0; j < 10; ++j) {
                const TString subdir2 = ToString(j);
                const TString dir2 = dir1 + LOCSLASH_C + subdir2;
                const TDirHier::TPath path2 = {dir2, 1};

                hier.Add(path2);

                for (size_t k = 0; k < 3; ++k) {
                    const TString file = dir2 + LOCSLASH_C + "file" + ToString(k);
                    const TDirHier::TPath fpath = {file, 0};

                    hier.Add(fpath);
                }
            }
        }

        TDirIterator d(dir);

        for (auto it = d.begin(); it != d.end(); ++it) {
            UNIT_ASSERT(hier.Have(it->fts_path, it->fts_info != FTS_F));
        }
    }

    inline void TestSkip() {
        TDirHier hier;

        const TString dir = "tmpdir";
        const TDirHier::TPath path = {dir, 1};

        hier.Add(path);
        hier.AddDir(dir + LOCSLASH_C + "dir1");
        hier.AddDir(dir + LOCSLASH_C + "dir1" + LOCSLASH_C + "dir2");
        //
        // Without skip
        //
        {
            TDirIterator di(dir);

            UNIT_ASSERT(di.Next());
            UNIT_ASSERT_EQUAL(TStringBuf(di.Next()->fts_name), "dir1");
            UNIT_ASSERT_EQUAL(TStringBuf(di.Next()->fts_name), "dir2");
            UNIT_ASSERT_EQUAL(TStringBuf(di.Next()->fts_name), "dir2");
            UNIT_ASSERT_EQUAL(TStringBuf(di.Next()->fts_name), "dir1");
            UNIT_ASSERT(di.Next());
            UNIT_ASSERT_EQUAL(di.Next(), nullptr);
        }
        //
        // With skip
        //
        {
            TDirIterator di(dir);

            UNIT_ASSERT(di.Next());
            auto ent = di.Next();
            UNIT_ASSERT_EQUAL(TStringBuf(ent->fts_name), "dir1");
            di.Skip(ent);
            UNIT_ASSERT_EQUAL(TStringBuf(di.Next()->fts_name), "dir1");
            UNIT_ASSERT(di.Next());
            UNIT_ASSERT_EQUAL(di.Next(), nullptr);
        }
    }

    inline void TestSort() {
        TDirHier dh;
        const TString dir("tmpdir");

        //prepare fs
        {
            TMersenne<ui32> rnd;
            const TString prefixes[] = {
                "a", "b", "xxx", "111", ""};

            dh.AddDir(dir);

            for (size_t i = 0; i < 100; ++i) {
                const TString fname = dir + LOCSLASH_C + prefixes[i % Y_ARRAY_SIZE(prefixes)] + ToString(rnd.GenRand());

                dh.AddFile(fname);
            }
        }

        TVector<TString> fnames;

        {
            TDirIterator d(dir, TDirIterator::TOptions().SetSortByName());

            for (auto it = d.begin(); it != d.end(); ++it) {
                if (it->fts_info == FTS_F) {
                    fnames.push_back(it->fts_name);
                }
            }
        }

        TVector<TString> sorted(fnames);
        Sort(sorted.begin(), sorted.end());

        UNIT_ASSERT_VALUES_EQUAL(JoinWithNewline(fnames), JoinWithNewline(sorted));
    }

    inline void TestError() {
        try {
            TDirIterator d("./notexistingfilename");

            UNIT_ASSERT(false);
        } catch (const TDirIterator::TError&) {
        } catch (...) {
            UNIT_ASSERT(false);
        }

        UNIT_ASSERT(true);
    }
};

UNIT_TEST_SUITE_REGISTRATION(TDirIteratorTest);
