#include <components/vfs/pathutil.hpp>

#include <gtest/gtest.h>

#include <sstream>

namespace VFS::Path
{
    namespace
    {
        using namespace testing;

        template <class T0, class T1>
        struct TypePair
        {
            using Type0 = T0;
            using Type1 = T1;
        };

        struct VFSPathIsNormalizedTest : TestWithParam<std::pair<std::string_view, bool>>
        {
        };

        TEST_P(VFSPathIsNormalizedTest, shouldReturnExpectedResult)
        {
            EXPECT_EQ(isNormalized(GetParam().first), GetParam().second);
        }

        const std::pair<std::string_view, bool> isNormalizedTestParams[] = {
            { std::string_view(), true },
            { "foo", true },
            { "foo/bar", true },
            { "foo/bar/baz", true },
            { "/foo", false },
            { "foo//", false },
            { "foo\\", false },
            { "Foo", false },
        };

        INSTANTIATE_TEST_SUITE_P(IsNormalizedTestParams, VFSPathIsNormalizedTest, ValuesIn(isNormalizedTestParams));

        TEST(VFSPathNormalizeFilenameInPlaceTest, shouldRemoveLeadingSeparators)
        {
            std::string value("//foo");
            normalizeFilenameInPlace(value);
            EXPECT_EQ(value, "foo");
        }

        TEST(VFSPathNormalizeFilenameInPlaceTest, shouldRemoveDuplicatedSeparators)
        {
            std::string value("foo//bar///baz");
            normalizeFilenameInPlace(value);
            EXPECT_EQ(value, "foo/bar/baz");
        }

        TEST(VFSPathNormalizeFilenameInPlaceTest, shouldRemoveDuplicatedLeadingSeparator)
        {
            std::string value("//foo");
            normalizeFilenameInPlace(value);
            EXPECT_EQ(value, "foo");
        }

        TEST(VFSPathExtensionViewTest, shouldSupportDefaultConstructor)
        {
            constexpr ExtensionView extension;
            EXPECT_TRUE(extension.empty());
            EXPECT_EQ(extension.value(), "");
        }

        TEST(VFSPathExtensionViewTest, shouldSupportConstexprConstructorFromConstCharPtr)
        {
            constexpr ExtensionView extension("png");
            EXPECT_FALSE(extension.empty());
            EXPECT_EQ(extension.value(), "png");
        }

        TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionOnNotNormalizedValue)
        {
            EXPECT_THROW([] { ExtensionView("PNG"); }(), std::invalid_argument);
        }

        TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionIfValueContainsExtensionSeparator)
        {
            EXPECT_THROW([] { ExtensionView(".png"); }(), std::invalid_argument);
        }

        TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionIfValueContainsSeparator)
        {
            EXPECT_THROW([] { ExtensionView("/png"); }(), std::invalid_argument);
        }

        template <class T>
        struct VFSPathExtensionViewOperatorsTest : Test
        {
        };

        TYPED_TEST_SUITE_P(VFSPathExtensionViewOperatorsTest);

        TYPED_TEST_P(VFSPathExtensionViewOperatorsTest, supportsEqual)
        {
            using Type0 = typename TypeParam::Type0;
            using Type1 = typename TypeParam::Type1;
            const Type0 extension{ "png" };
            const Type1 otherEqual{ "png" };
            const Type1 otherNotEqual{ "jpg" };
            EXPECT_EQ(extension, otherEqual);
            EXPECT_EQ(otherEqual, extension);
            EXPECT_NE(extension, otherNotEqual);
            EXPECT_NE(otherNotEqual, extension);
        }

        TYPED_TEST_P(VFSPathExtensionViewOperatorsTest, supportsLess)
        {
            using Type0 = typename TypeParam::Type0;
            using Type1 = typename TypeParam::Type1;
            const Type0 extension{ "png" };
            const Type1 otherEqual{ "png" };
            const Type1 otherLess{ "jpg" };
            const Type1 otherGreater{ "tga" };
            EXPECT_FALSE(extension < otherEqual);
            EXPECT_FALSE(otherEqual < extension);
            EXPECT_LT(otherLess, extension);
            EXPECT_FALSE(extension < otherLess);
            EXPECT_LT(extension, otherGreater);
            EXPECT_FALSE(otherGreater < extension);
        }

        REGISTER_TYPED_TEST_SUITE_P(VFSPathExtensionViewOperatorsTest, supportsEqual, supportsLess);

        using VFSPathExtensionViewOperatorsTypePairs
            = Types<TypePair<ExtensionView, ExtensionView>, TypePair<ExtensionView, const char*>,
                TypePair<ExtensionView, std::string>, TypePair<ExtensionView, std::string_view>>;

        INSTANTIATE_TYPED_TEST_SUITE_P(
            Typed, VFSPathExtensionViewOperatorsTest, VFSPathExtensionViewOperatorsTypePairs);

        TEST(VFSPathNormalizedTest, shouldSupportDefaultConstructor)
        {
            const Normalized value;
            EXPECT_EQ(value.value(), "");
        }

        TEST(VFSPathNormalizedTest, shouldSupportConstructorFromString)
        {
            const std::string string("Foo\\Bar/baz");
            const Normalized value(string);
            EXPECT_EQ(value.value(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedTest, shouldSupportConstructorFromConstCharPtr)
        {
            const char* const ptr = "Foo\\Bar/baz";
            const Normalized value(ptr);
            EXPECT_EQ(value.value(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedTest, shouldSupportConstructorFromStringView)
        {
            const std::string_view view = "Foo\\Bar/baz";
            const Normalized value(view);
            EXPECT_EQ(value.view(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedTest, shouldSupportConstructorFromNormalizedView)
        {
            const NormalizedView view("foo/bar/baz");
            const Normalized value(view);
            EXPECT_EQ(value.view(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedTest, supportMovingValueOut)
        {
            Normalized value("Foo\\Bar/baz");
            EXPECT_EQ(std::move(value).value(), "foo/bar/baz");
            EXPECT_EQ(value.value(), "");
        }

        TEST(VFSPathNormalizedTest, isNotEqualToNotNormalized)
        {
            const Normalized value("Foo\\Bar/baz");
            EXPECT_NE(value.value(), "Foo\\Bar/baz");
        }

        TEST(VFSPathNormalizedTest, shouldSupportOperatorLeftShiftToOStream)
        {
            const Normalized value("Foo\\Bar/baz");
            std::stringstream stream;
            stream << value;
            EXPECT_EQ(stream.str(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedTest, shouldSupportOperatorDivEqual)
        {
            Normalized value("foo/bar");
            value /= NormalizedView("baz");
            EXPECT_EQ(value.value(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedTest, shouldSupportOperatorDivEqualWithStringView)
        {
            Normalized value("foo/bar");
            value /= std::string_view("BAZ");
            EXPECT_EQ(value.value(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedTest, operatorDivShouldNormalizeSuffix)
        {
            Normalized value("foo/bar");
            value /= std::string_view("\\A\\\\B");
            EXPECT_EQ(value.value(), "foo/bar/a/b");
        }

        TEST(VFSPathNormalizedTest, operatorDivForNormalizedViewShouldNotPrependSeparatorToEmpty)
        {
            Normalized value;
            value /= NormalizedView("a");
            EXPECT_EQ(value.value(), "a");
        }

        TEST(VFSPathNormalizedTest, operatorDivForStringViewShouldNotPrependSeparatorToEmpty)
        {
            Normalized value;
            value /= std::string_view("a");
            EXPECT_EQ(value.value(), "a");
        }

        TEST(VFSPathNormalizedTest, changeExtensionShouldReplaceAfterLastDot)
        {
            Normalized value("foo/ba.r.a");
            constexpr ExtensionView extension("so");
            ASSERT_TRUE(value.changeExtension(extension));
            EXPECT_EQ(value.value(), "foo/ba.r.so");
        }

        TEST(VFSPathNormalizedTest, changeExtensionShouldIgnorePathWithoutADot)
        {
            Normalized value("foo/bar");
            constexpr ExtensionView extension("so");
            ASSERT_FALSE(value.changeExtension(extension));
            EXPECT_EQ(value.value(), "foo/bar");
        }

        TEST(VFSPathNormalizedTest, changeExtensionShouldIgnorePathWithDotBeforeSeparator)
        {
            Normalized value("foo.bar/baz");
            constexpr ExtensionView extension("so");
            ASSERT_FALSE(value.changeExtension(extension));
            EXPECT_EQ(value.value(), "foo.bar/baz");
        }

        TEST(VFSPathNormalizedTest, changeExtensionShouldReplaceWithShorterExtension)
        {
            Normalized value("foo/bar.nif");
            constexpr ExtensionView extension("kf");
            ASSERT_TRUE(value.changeExtension(extension));
            EXPECT_EQ(value.value(), "foo/bar.kf");
        }

        TEST(VFSPathNormalizedTest, filenameShouldReturnLastComponentOfThePath)
        {
            const Normalized value("foo/bar");
            EXPECT_EQ(value.filename(), "bar");
        }

        TEST(VFSPathNormalizedTest, filenameShouldReturnSameValueForPathWithSingleComponent)
        {
            const Normalized value("foo");
            EXPECT_EQ(value.filename(), "foo");
        }

        TEST(VFSPathNormalizedTest, appendShouldAddExtensionWithSeparator)
        {
            Normalized value("foo");
            value.append(ExtensionView("ext"));
            EXPECT_EQ(value.value(), "foo.ext");
        }

        TEST(VFSPathNormalizedTest, appendForEmptyShouldAddExtensionWithSeparator)
        {
            Normalized value;
            value.append(ExtensionView("ext"));
            EXPECT_EQ(value.value(), ".ext");
        }

        template <class T>
        struct VFSPathNormalizedOperatorsTest : Test
        {
        };

        TYPED_TEST_SUITE_P(VFSPathNormalizedOperatorsTest);

        TYPED_TEST_P(VFSPathNormalizedOperatorsTest, supportsEqual)
        {
            using Type0 = typename TypeParam::Type0;
            using Type1 = typename TypeParam::Type1;
            const Type0 normalized{ "a/foo/bar/baz" };
            const Type1 otherEqual{ "a/foo/bar/baz" };
            const Type1 otherNotEqual{ "b/foo/bar/baz" };
            EXPECT_EQ(normalized, otherEqual);
            EXPECT_EQ(otherEqual, normalized);
            EXPECT_NE(normalized, otherNotEqual);
            EXPECT_NE(otherNotEqual, normalized);
        }

        TYPED_TEST_P(VFSPathNormalizedOperatorsTest, supportsLess)
        {
            using Type0 = typename TypeParam::Type0;
            using Type1 = typename TypeParam::Type1;
            const Type0 normalized{ "b/foo/bar/baz" };
            const Type1 otherEqual{ "b/foo/bar/baz" };
            const Type1 otherLess{ "a/foo/bar/baz" };
            const Type1 otherGreater{ "c/foo/bar/baz" };
            EXPECT_FALSE(normalized < otherEqual);
            EXPECT_FALSE(otherEqual < normalized);
            EXPECT_LT(otherLess, normalized);
            EXPECT_FALSE(normalized < otherLess);
            EXPECT_LT(normalized, otherGreater);
            EXPECT_FALSE(otherGreater < normalized);
        }

        REGISTER_TYPED_TEST_SUITE_P(VFSPathNormalizedOperatorsTest, supportsEqual, supportsLess);

        using VFSPathNormalizedOperatorsTypePairs
            = Types<TypePair<Normalized, Normalized>, TypePair<Normalized, const char*>,
                TypePair<Normalized, std::string>, TypePair<Normalized, std::string_view>,
                TypePair<Normalized, NormalizedView>, TypePair<NormalizedView, Normalized>,
                TypePair<NormalizedView, const char*>, TypePair<NormalizedView, std::string>,
                TypePair<NormalizedView, std::string_view>, TypePair<NormalizedView, NormalizedView>>;

        INSTANTIATE_TYPED_TEST_SUITE_P(Typed, VFSPathNormalizedOperatorsTest, VFSPathNormalizedOperatorsTypePairs);

        TEST(VFSPathNormalizedViewTest, shouldSupportConstructorFromNormalized)
        {
            const Normalized value("Foo\\Bar/baz");
            const NormalizedView view(value);
            EXPECT_EQ(view.value(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedViewTest, shouldSupportConstexprConstructorFromNormalizedStringLiteral)
        {
            constexpr NormalizedView view("foo/bar/baz");
            EXPECT_EQ(view.value(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedViewTest, constructorShouldThrowExceptionOnNotNormalized)
        {
            EXPECT_THROW([] { NormalizedView("Foo\\Bar/baz"); }(), std::invalid_argument);
        }

        TEST(VFSPathNormalizedViewTest, shouldSupportOperatorDiv)
        {
            const NormalizedView a("foo/bar");
            const NormalizedView b("baz");
            const Normalized result = a / b;
            EXPECT_EQ(result.value(), "foo/bar/baz");
        }

        TEST(VFSPathNormalizedViewTest, filenameShouldReturnLastComponentOfThePath)
        {
            const NormalizedView value("foo/bar");
            EXPECT_EQ(value.filename(), "bar");
        }

        TEST(VFSPathNormalizedViewTest, filenameShouldReturnSameValueForPathWithSingleComponent)
        {
            const NormalizedView value("foo");
            EXPECT_EQ(value.filename(), "foo");
        }

        TEST(VFSPathNormalizedViewTest, stemShouldOmitPathAndExtension)
        {
            const NormalizedView value("foo/bar.a");
            EXPECT_EQ(value.stem(), "bar");
        }

        TEST(VFSPathNormalizedViewTest, stemShouldReturnSameValueForPathWithSingleComponent)
        {
            const NormalizedView value("foo");
            EXPECT_EQ(value.stem(), "foo");
        }

        TEST(VFSPathNormalizedViewTest, stemShouldIncludeDotsBeforeTheExtension)
        {
            const NormalizedView value("last voyage of the u.s.s. constitution.swf");
            EXPECT_EQ(value.stem(), "last voyage of the u.s.s. constitution");
        }

        TEST(VFSPathNormalizedJoinTest, shouldHandleSingleValue)
        {
            EXPECT_EQ(join(Normalized("foo")), "foo");
        }

        TEST(VFSPathNormalizedJoinTest, shouldHandleSingleView)
        {
            EXPECT_EQ(join(NormalizedView("foo")), "foo");
        }

        TEST(VFSPathNormalizedJoinTest, shouldHandleSingleString)
        {
            EXPECT_EQ(join(std::string("FOO")), "foo");
        }

        TEST(VFSPathNormalizedJoinTest, shouldHandleSingleStringView)
        {
            EXPECT_EQ(join(std::string_view("FOO")), "foo");
        }

        TEST(VFSPathNormalizedJoinTest, shouldHandleSingleConstCharArray)
        {
            EXPECT_EQ(join("FOO"), "foo");
        }

        TEST(VFSPathNormalizedJoinTest, shouldHandleSingleConstCharPtr)
        {
            const char* const ptr = "FOO";
            EXPECT_EQ(join(ptr), "foo");
        }

        TEST(VFSPathNormalizedJoinTest, shouldHandleSingleExtension)
        {
            constexpr ExtensionView extension("foo");
            EXPECT_EQ(join(extension), ".foo");
        }

        TEST(VFSPathNormalizedJoinTest, shouldJoinTwoPathComponents)
        {
            EXPECT_EQ(join(NormalizedView("foo"), std::string_view("BAR")), "foo/bar");
        }

        TEST(VFSPathNormalizedJoinTest, shouldJoinComponentAndExtension)
        {
            EXPECT_EQ(join(NormalizedView("foo"), ExtensionView("bar")), "foo.bar");
        }

        TEST(VFSPathNormalizedJoinTest, shouldJoinTwoExtensions)
        {
            EXPECT_EQ(join(ExtensionView("foo"), ExtensionView("bar")), ".foo.bar");
        }

        TEST(VFSPathNormalizedJoinTest, shouldJoinTreePathComponents)
        {
            EXPECT_EQ(join(NormalizedView("a"), std::string_view("B"), Normalized("c")), "a/b/c");
        }

        TEST(VFSPathNormalizedJoinedCapacityTest, forNormalizedShouldReturnSizeOnly)
        {
            EXPECT_EQ(joinedCapacity(Normalized("a")), 1);
        }

        TEST(VFSPathNormalizedJoinedCapacityTest, forConstCharPtrShouldReturnSizeOnly)
        {
            EXPECT_EQ(joinedCapacity("a"), 1);
        }

        TEST(VFSPathNormalizedJoinedCapacityTest, forExtensionViewShouldReturnSizePlusSeparator)
        {
            EXPECT_EQ(joinedCapacity(ExtensionView("a")), 2);
        }

        TEST(VFSPathNormalizedJoinedCapacityTest, shouldReturnSumOfSizesPlusNumberOfSeparators)
        {
            constexpr NormalizedView b("b");
            const Normalized c("c");
            constexpr std::string_view d("d");
            const std::string e("e");
            constexpr ExtensionView f("f");
            EXPECT_EQ(joinedCapacity("a", b, c, d, e, f), 11);
        }
    }
}
