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

#include "eval-settings.hh"
#include "memory-source-accessor.hh"

#include "tests/libexpr.hh"

namespace nix {
    class CaptureLogger : public Logger
    {
        std::ostringstream oss;

        public:
            CaptureLogger() {}

            std::string get() const {
                return oss.str();
            }

            void log(Verbosity lvl, std::string_view s) override {
                oss << s << std::endl;
            }

            void logEI(const ErrorInfo & ei) override {
                showErrorInfo(oss, ei, loggerSettings.showTrace.get());
            }
    };

    class CaptureLogging {
        Logger * oldLogger;
        std::unique_ptr<CaptureLogger> tempLogger;
        public:
            CaptureLogging() : tempLogger(std::make_unique<CaptureLogger>()) {
                oldLogger = logger;
                logger = tempLogger.get();
            }

            ~CaptureLogging() {
                logger = oldLogger;
            }

            std::string get() const {
                return tempLogger->get();
            }
    };


    // Testing eval of PrimOp's
    class PrimOpTest : public LibExprTest {};


    TEST_F(PrimOpTest, throw) {
        ASSERT_THROW(eval("throw \"foo\""), ThrownError);
    }

    TEST_F(PrimOpTest, abort) {
        ASSERT_THROW(eval("abort \"abort\""), Abort);
    }

    TEST_F(PrimOpTest, ceil) {
        auto v = eval("builtins.ceil 1.9");
        ASSERT_THAT(v, IsIntEq(2));
    }

    TEST_F(PrimOpTest, floor) {
        auto v = eval("builtins.floor 1.9");
        ASSERT_THAT(v, IsIntEq(1));
    }

    TEST_F(PrimOpTest, tryEvalFailure) {
        auto v = eval("builtins.tryEval (throw \"\")");
        ASSERT_THAT(v, IsAttrsOfSize(2));
        auto s = createSymbol("success");
        auto p = v.attrs()->get(s);
        ASSERT_NE(p, nullptr);
        ASSERT_THAT(*p->value, IsFalse());
    }

    TEST_F(PrimOpTest, tryEvalSuccess) {
        auto v = eval("builtins.tryEval 123");
        ASSERT_THAT(v, IsAttrs());
        auto s = createSymbol("success");
        auto p = v.attrs()->get(s);
        ASSERT_NE(p, nullptr);
        ASSERT_THAT(*p->value, IsTrue());
        s = createSymbol("value");
        p = v.attrs()->get(s);
        ASSERT_NE(p, nullptr);
        ASSERT_THAT(*p->value, IsIntEq(123));
    }

    TEST_F(PrimOpTest, getEnv) {
        setEnv("_NIX_UNIT_TEST_ENV_VALUE", "test value");
        auto v = eval("builtins.getEnv \"_NIX_UNIT_TEST_ENV_VALUE\"");
        ASSERT_THAT(v, IsStringEq("test value"));
    }

    TEST_F(PrimOpTest, seq) {
        ASSERT_THROW(eval("let x = throw \"test\"; in builtins.seq x { }"), ThrownError);
    }

    TEST_F(PrimOpTest, seqNotDeep) {
        auto v = eval("let x = { z =  throw \"test\"; }; in builtins.seq x { }");
        ASSERT_THAT(v, IsAttrs());
    }

    TEST_F(PrimOpTest, deepSeq) {
        ASSERT_THROW(eval("let x = { z =  throw \"test\"; }; in builtins.deepSeq x { }"), ThrownError);
    }

    TEST_F(PrimOpTest, trace) {
        CaptureLogging l;
        auto v = eval("builtins.trace \"test string 123\" 123");
        ASSERT_THAT(v, IsIntEq(123));
        auto text = l.get();
        ASSERT_NE(text.find("test string 123"), std::string::npos);
    }

    TEST_F(PrimOpTest, placeholder) {
        auto v = eval("builtins.placeholder \"out\"");
        ASSERT_THAT(v, IsStringEq("/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"));
    }

    TEST_F(PrimOpTest, baseNameOf) {
        auto v = eval("builtins.baseNameOf /some/path");
        ASSERT_THAT(v, IsStringEq("path"));
    }

    TEST_F(PrimOpTest, dirOf) {
        auto v = eval("builtins.dirOf /some/path");
        ASSERT_THAT(v, IsPathEq("/some"));
    }

    TEST_F(PrimOpTest, attrValues) {
        auto v = eval("builtins.attrValues { x = \"foo\";  a = 1; }");
        ASSERT_THAT(v, IsListOfSize(2));
        ASSERT_THAT(*v.listElems()[0], IsIntEq(1));
        ASSERT_THAT(*v.listElems()[1], IsStringEq("foo"));
    }

    TEST_F(PrimOpTest, getAttr) {
        auto v = eval("builtins.getAttr \"x\" { x = \"foo\"; }");
        ASSERT_THAT(v, IsStringEq("foo"));
    }

    TEST_F(PrimOpTest, getAttrNotFound) {
        // FIXME: TypeError is really bad here, also the error wording is worse
        // than on Nix <=2.3
        ASSERT_THROW(eval("builtins.getAttr \"y\" { }"), TypeError);
    }

    TEST_F(PrimOpTest, unsafeGetAttrPos) {
        state.corepkgsFS->addFile(CanonPath("foo.nix"), "\n\r\n\r{ y = \"x\"; }");

        auto expr = "builtins.unsafeGetAttrPos \"y\" (import <nix/foo.nix>)";
        auto v = eval(expr);
        ASSERT_THAT(v, IsAttrsOfSize(3));

        auto file = v.attrs()->find(createSymbol("file"));
        ASSERT_NE(file, nullptr);
        ASSERT_THAT(*file->value, IsString());
        auto s = baseNameOf(file->value->string_view());
        ASSERT_EQ(s, "foo.nix");

        auto line = v.attrs()->find(createSymbol("line"));
        ASSERT_NE(line, nullptr);
        state.forceValue(*line->value, noPos);
        ASSERT_THAT(*line->value, IsIntEq(4));

        auto column = v.attrs()->find(createSymbol("column"));
        ASSERT_NE(column, nullptr);
        state.forceValue(*column->value, noPos);
        ASSERT_THAT(*column->value, IsIntEq(3));
    }

    TEST_F(PrimOpTest, hasAttr) {
        auto v = eval("builtins.hasAttr \"x\" { x = 1; }");
        ASSERT_THAT(v, IsTrue());
    }

    TEST_F(PrimOpTest, hasAttrNotFound) {
        auto v = eval("builtins.hasAttr \"x\" { }");
        ASSERT_THAT(v, IsFalse());
    }

    TEST_F(PrimOpTest, isAttrs) {
        auto v = eval("builtins.isAttrs {}");
        ASSERT_THAT(v, IsTrue());
    }

    TEST_F(PrimOpTest, isAttrsFalse) {
        auto v = eval("builtins.isAttrs null");
        ASSERT_THAT(v, IsFalse());
    }

    TEST_F(PrimOpTest, removeAttrs) {
        auto v = eval("builtins.removeAttrs { x = 1; } [\"x\"]");
        ASSERT_THAT(v, IsAttrsOfSize(0));
    }

    TEST_F(PrimOpTest, removeAttrsRetains) {
        auto v = eval("builtins.removeAttrs { x = 1; y = 2; } [\"x\"]");
        ASSERT_THAT(v, IsAttrsOfSize(1));
        ASSERT_NE(v.attrs()->find(createSymbol("y")), nullptr);
    }

    TEST_F(PrimOpTest, listToAttrsEmptyList) {
        auto v = eval("builtins.listToAttrs []");
        ASSERT_THAT(v, IsAttrsOfSize(0));
        ASSERT_EQ(v.type(), nAttrs);
        ASSERT_EQ(v.attrs()->size(), 0);
    }

    TEST_F(PrimOpTest, listToAttrsNotFieldName) {
        ASSERT_THROW(eval("builtins.listToAttrs [{}]"), Error);
    }

    TEST_F(PrimOpTest, listToAttrs) {
        auto v = eval("builtins.listToAttrs [ { name = \"key\"; value = 123; } ]");
        ASSERT_THAT(v, IsAttrsOfSize(1));
        auto key = v.attrs()->find(createSymbol("key"));
        ASSERT_NE(key, nullptr);
        ASSERT_THAT(*key->value, IsIntEq(123));
    }

    TEST_F(PrimOpTest, intersectAttrs) {
        auto v = eval("builtins.intersectAttrs { a = 1; b = 2; } { b = 3; c = 4; }");
        ASSERT_THAT(v, IsAttrsOfSize(1));
        auto b = v.attrs()->find(createSymbol("b"));
        ASSERT_NE(b, nullptr);
        ASSERT_THAT(*b->value, IsIntEq(3));
    }

    TEST_F(PrimOpTest, catAttrs) {
        auto v = eval("builtins.catAttrs \"a\" [{a = 1;} {b = 0;} {a = 2;}]");
        ASSERT_THAT(v, IsListOfSize(2));
        ASSERT_THAT(*v.listElems()[0], IsIntEq(1));
        ASSERT_THAT(*v.listElems()[1], IsIntEq(2));
    }

    TEST_F(PrimOpTest, functionArgs) {
        auto v = eval("builtins.functionArgs ({ x, y ? 123}: 1)");
        ASSERT_THAT(v, IsAttrsOfSize(2));

        auto x = v.attrs()->find(createSymbol("x"));
        ASSERT_NE(x, nullptr);
        ASSERT_THAT(*x->value, IsFalse());

        auto y = v.attrs()->find(createSymbol("y"));
        ASSERT_NE(y, nullptr);
        ASSERT_THAT(*y->value, IsTrue());
    }

    TEST_F(PrimOpTest, mapAttrs) {
        auto v = eval("builtins.mapAttrs (name: value: value * 10) { a = 1; b = 2; }");
        ASSERT_THAT(v, IsAttrsOfSize(2));

        auto a = v.attrs()->find(createSymbol("a"));
        ASSERT_NE(a, nullptr);
        ASSERT_THAT(*a->value, IsThunk());
        state.forceValue(*a->value, noPos);
        ASSERT_THAT(*a->value, IsIntEq(10));

        auto b = v.attrs()->find(createSymbol("b"));
        ASSERT_NE(b, nullptr);
        ASSERT_THAT(*b->value, IsThunk());
        state.forceValue(*b->value, noPos);
        ASSERT_THAT(*b->value, IsIntEq(20));
    }

    TEST_F(PrimOpTest, isList) {
        auto v = eval("builtins.isList []");
        ASSERT_THAT(v, IsTrue());
    }

    TEST_F(PrimOpTest, isListFalse) {
        auto v = eval("builtins.isList null");
        ASSERT_THAT(v, IsFalse());
    }

    TEST_F(PrimOpTest, elemtAt) {
        auto v = eval("builtins.elemAt [0 1 2 3] 3");
        ASSERT_THAT(v, IsIntEq(3));
    }

    TEST_F(PrimOpTest, elemtAtOutOfBounds) {
        ASSERT_THROW(eval("builtins.elemAt [0 1 2 3] 5"), Error);
    }

    TEST_F(PrimOpTest, head) {
        auto v = eval("builtins.head [ 3 2 1 0 ]");
        ASSERT_THAT(v, IsIntEq(3));
    }

    TEST_F(PrimOpTest, headEmpty) {
        ASSERT_THROW(eval("builtins.head [ ]"), Error);
    }

    TEST_F(PrimOpTest, headWrongType) {
        ASSERT_THROW(eval("builtins.head { }"), Error);
    }

    TEST_F(PrimOpTest, tail) {
        auto v = eval("builtins.tail [ 3 2 1 0 ]");
        ASSERT_THAT(v, IsListOfSize(3));
        for (const auto [n, elem] : enumerate(v.listItems()))
            ASSERT_THAT(*elem, IsIntEq(2 - static_cast<int>(n)));
    }

    TEST_F(PrimOpTest, tailEmpty) {
        ASSERT_THROW(eval("builtins.tail []"), Error);
    }

    TEST_F(PrimOpTest, map) {
        auto v = eval("map (x: \"foo\" + x) [ \"bar\" \"bla\" \"abc\" ]");
        ASSERT_THAT(v, IsListOfSize(3));
        auto elem = v.listElems()[0];
        ASSERT_THAT(*elem, IsThunk());
        state.forceValue(*elem, noPos);
        ASSERT_THAT(*elem, IsStringEq("foobar"));

        elem = v.listElems()[1];
        ASSERT_THAT(*elem, IsThunk());
        state.forceValue(*elem, noPos);
        ASSERT_THAT(*elem, IsStringEq("foobla"));

        elem = v.listElems()[2];
        ASSERT_THAT(*elem, IsThunk());
        state.forceValue(*elem, noPos);
        ASSERT_THAT(*elem, IsStringEq("fooabc"));
    }

    TEST_F(PrimOpTest, filter) {
        auto v = eval("builtins.filter (x: x == 2) [ 3 2 3 2 3 2 ]");
        ASSERT_THAT(v, IsListOfSize(3));
        for (const auto elem : v.listItems())
            ASSERT_THAT(*elem, IsIntEq(2));
    }

    TEST_F(PrimOpTest, elemTrue) {
        auto v = eval("builtins.elem 3 [ 1 2 3 4 5 ]");
        ASSERT_THAT(v, IsTrue());
    }

    TEST_F(PrimOpTest, elemFalse) {
        auto v = eval("builtins.elem 6 [ 1 2 3 4 5 ]");
        ASSERT_THAT(v, IsFalse());
    }

    TEST_F(PrimOpTest, concatLists) {
        auto v = eval("builtins.concatLists [[1 2] [3 4]]");
        ASSERT_THAT(v, IsListOfSize(4));
        for (const auto [i, elem] : enumerate(v.listItems()))
            ASSERT_THAT(*elem, IsIntEq(static_cast<int>(i)+1));
    }

    TEST_F(PrimOpTest, length) {
        auto v = eval("builtins.length [ 1 2 3 ]");
        ASSERT_THAT(v, IsIntEq(3));
    }

    TEST_F(PrimOpTest, foldStrict) {
        auto v = eval("builtins.foldl' (a: b: a + b) 0 [1 2 3]");
        ASSERT_THAT(v, IsIntEq(6));
    }

    TEST_F(PrimOpTest, anyTrue) {
        auto v = eval("builtins.any (x: x == 2) [ 1 2 3 ]");
        ASSERT_THAT(v, IsTrue());
    }

    TEST_F(PrimOpTest, anyFalse) {
        auto v = eval("builtins.any (x: x == 5) [ 1 2 3 ]");
        ASSERT_THAT(v, IsFalse());
    }

    TEST_F(PrimOpTest, allTrue) {
        auto v = eval("builtins.all (x: x > 0) [ 1 2 3 ]");
        ASSERT_THAT(v, IsTrue());
    }

    TEST_F(PrimOpTest, allFalse) {
        auto v = eval("builtins.all (x: x <= 0) [ 1 2 3 ]");
        ASSERT_THAT(v, IsFalse());
    }

    TEST_F(PrimOpTest, genList) {
        auto v = eval("builtins.genList (x: x + 1) 3");
        ASSERT_EQ(v.type(), nList);
        ASSERT_EQ(v.listSize(), 3);
        for (const auto [i, elem] : enumerate(v.listItems())) {
            ASSERT_THAT(*elem, IsThunk());
            state.forceValue(*elem, noPos);
            ASSERT_THAT(*elem, IsIntEq(static_cast<int>(i)+1));
        }
    }

    TEST_F(PrimOpTest, sortLessThan) {
        auto v = eval("builtins.sort builtins.lessThan [ 483 249 526 147 42 77 ]");
        ASSERT_EQ(v.type(), nList);
        ASSERT_EQ(v.listSize(), 6);

        const std::vector<int> numbers = { 42, 77, 147, 249, 483, 526 };
        for (const auto [n, elem] : enumerate(v.listItems()))
            ASSERT_THAT(*elem, IsIntEq(numbers[n]));
    }

    TEST_F(PrimOpTest, partition) {
        auto v = eval("builtins.partition (x: x > 10) [1 23 9 3 42]");
        ASSERT_THAT(v, IsAttrsOfSize(2));

        auto right = v.attrs()->get(createSymbol("right"));
        ASSERT_NE(right, nullptr);
        ASSERT_THAT(*right->value, IsListOfSize(2));
        ASSERT_THAT(*right->value->listElems()[0], IsIntEq(23));
        ASSERT_THAT(*right->value->listElems()[1], IsIntEq(42));

        auto wrong = v.attrs()->get(createSymbol("wrong"));
        ASSERT_NE(wrong, nullptr);
        ASSERT_EQ(wrong->value->type(), nList);
        ASSERT_EQ(wrong->value->listSize(), 3);
        ASSERT_THAT(*wrong->value, IsListOfSize(3));
        ASSERT_THAT(*wrong->value->listElems()[0], IsIntEq(1));
        ASSERT_THAT(*wrong->value->listElems()[1], IsIntEq(9));
        ASSERT_THAT(*wrong->value->listElems()[2], IsIntEq(3));
    }

    TEST_F(PrimOpTest, concatMap) {
        auto v = eval("builtins.concatMap (x: x ++ [0]) [ [1 2] [3 4] ]");
        ASSERT_EQ(v.type(), nList);
        ASSERT_EQ(v.listSize(), 6);

        const std::vector<int> numbers = { 1, 2, 0, 3, 4, 0 };
        for (const auto [n, elem] : enumerate(v.listItems()))
            ASSERT_THAT(*elem, IsIntEq(numbers[n]));
    }

    TEST_F(PrimOpTest, addInt) {
        auto v = eval("builtins.add 3 5");
        ASSERT_THAT(v, IsIntEq(8));
    }

    TEST_F(PrimOpTest, addFloat) {
        auto v = eval("builtins.add 3.0 5.0");
        ASSERT_THAT(v, IsFloatEq(8.0));
    }

    TEST_F(PrimOpTest, addFloatToInt) {
        auto v = eval("builtins.add 3.0 5");
        ASSERT_THAT(v, IsFloatEq(8.0));

        v = eval("builtins.add 3 5.0");
        ASSERT_THAT(v, IsFloatEq(8.0));
    }

    TEST_F(PrimOpTest, subInt) {
        auto v = eval("builtins.sub 5 2");
        ASSERT_THAT(v, IsIntEq(3));
    }

    TEST_F(PrimOpTest, subFloat) {
        auto v = eval("builtins.sub 5.0 2.0");
        ASSERT_THAT(v, IsFloatEq(3.0));
    }

    TEST_F(PrimOpTest, subFloatFromInt) {
        auto v = eval("builtins.sub 5.0 2");
        ASSERT_THAT(v, IsFloatEq(3.0));

        v = eval("builtins.sub 4 2.0");
        ASSERT_THAT(v, IsFloatEq(2.0));
    }

    TEST_F(PrimOpTest, mulInt) {
        auto v = eval("builtins.mul 3 5");
        ASSERT_THAT(v, IsIntEq(15));
    }

    TEST_F(PrimOpTest, mulFloat) {
        auto v = eval("builtins.mul 3.0 5.0");
        ASSERT_THAT(v, IsFloatEq(15.0));
    }

    TEST_F(PrimOpTest, mulFloatMixed) {
        auto v = eval("builtins.mul 3 5.0");
        ASSERT_THAT(v, IsFloatEq(15.0));

        v = eval("builtins.mul 2.0 5");
        ASSERT_THAT(v, IsFloatEq(10.0));
    }

    TEST_F(PrimOpTest, divInt) {
        auto v = eval("builtins.div 5 (-1)");
        ASSERT_THAT(v, IsIntEq(-5));
    }

    TEST_F(PrimOpTest, divIntZero) {
        ASSERT_THROW(eval("builtins.div 5 0"), EvalError);
    }

    TEST_F(PrimOpTest, divFloat) {
        auto v = eval("builtins.div 5.0 (-1)");
        ASSERT_THAT(v, IsFloatEq(-5.0));
    }

    TEST_F(PrimOpTest, divFloatZero) {
        ASSERT_THROW(eval("builtins.div 5.0 0.0"), EvalError);
    }

    TEST_F(PrimOpTest, bitOr) {
        auto v = eval("builtins.bitOr 1 2");
        ASSERT_THAT(v, IsIntEq(3));
    }

    TEST_F(PrimOpTest, bitXor) {
        auto v = eval("builtins.bitXor 3 2");
        ASSERT_THAT(v, IsIntEq(1));
    }

    TEST_F(PrimOpTest, lessThanFalse) {
        auto v = eval("builtins.lessThan 3 1");
        ASSERT_THAT(v, IsFalse());
    }

    TEST_F(PrimOpTest, lessThanTrue) {
        auto v = eval("builtins.lessThan 1 3");
        ASSERT_THAT(v, IsTrue());
    }

    TEST_F(PrimOpTest, toStringAttrsThrows) {
        ASSERT_THROW(eval("builtins.toString {}"), EvalError);
    }

    TEST_F(PrimOpTest, toStringLambdaThrows) {
        ASSERT_THROW(eval("builtins.toString (x: x)"), EvalError);
    }

    class ToStringPrimOpTest :
        public PrimOpTest,
        public testing::WithParamInterface<std::tuple<std::string, std::string_view>>
        {};

    TEST_P(ToStringPrimOpTest, toString) {
        const auto [input, output] = GetParam();
        auto v = eval(input);
        ASSERT_THAT(v, IsStringEq(output));
    }

#define CASE(input, output) (std::make_tuple(std::string_view("builtins.toString " input), std::string_view(output)))
    INSTANTIATE_TEST_SUITE_P(
            toString,
            ToStringPrimOpTest,
            testing::Values(
                CASE(R"("foo")", "foo"),
                CASE(R"(1)", "1"),
                CASE(R"([1 2 3])", "1 2 3"),
                CASE(R"(.123)", "0.123000"),
                CASE(R"(true)", "1"),
                CASE(R"(false)", ""),
                CASE(R"(null)", ""),
                CASE(R"({ v = "bar"; __toString = self: self.v; })", "bar"),
                CASE(R"({ v = "bar"; __toString = self: self.v; outPath = "foo"; })", "bar"),
                CASE(R"({ outPath = "foo"; })", "foo"),
                CASE(R"(./test)", "/test")
            )
    );
#undef CASE

    TEST_F(PrimOpTest, substring){
        auto v = eval("builtins.substring 0 3 \"nixos\"");
        ASSERT_THAT(v, IsStringEq("nix"));
    }

    TEST_F(PrimOpTest, substringSmallerString){
        auto v = eval("builtins.substring 0 3 \"n\"");
        ASSERT_THAT(v, IsStringEq("n"));
    }

    TEST_F(PrimOpTest, substringEmptyString){
        auto v = eval("builtins.substring 1 3 \"\"");
        ASSERT_THAT(v, IsStringEq(""));
    }

    TEST_F(PrimOpTest, stringLength) {
        auto v = eval("builtins.stringLength \"123\"");
        ASSERT_THAT(v, IsIntEq(3));
    }
    TEST_F(PrimOpTest, hashStringMd5) {
        auto v = eval("builtins.hashString \"md5\" \"asdf\"");
        ASSERT_THAT(v, IsStringEq("912ec803b2ce49e4a541068d495ab570"));
    }

    TEST_F(PrimOpTest, hashStringSha1) {
        auto v = eval("builtins.hashString \"sha1\" \"asdf\"");
        ASSERT_THAT(v, IsStringEq("3da541559918a808c2402bba5012f6c60b27661c"));
    }

    TEST_F(PrimOpTest, hashStringSha256) {
        auto v = eval("builtins.hashString \"sha256\" \"asdf\"");
        ASSERT_THAT(v, IsStringEq("f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"));
    }

    TEST_F(PrimOpTest, hashStringSha512) {
        auto v = eval("builtins.hashString \"sha512\" \"asdf\"");
        ASSERT_THAT(v, IsStringEq("401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"));
    }

    TEST_F(PrimOpTest, hashStringInvalidHashAlgorithm) {
        ASSERT_THROW(eval("builtins.hashString \"foobar\" \"asdf\""), Error);
    }

    TEST_F(PrimOpTest, nixPath) {
        auto v = eval("builtins.nixPath");
        ASSERT_EQ(v.type(), nList);
        // We can't test much more as currently the EvalSettings are a global
        // that we can't easily swap / replace
    }

    TEST_F(PrimOpTest, langVersion) {
        auto v = eval("builtins.langVersion");
        ASSERT_EQ(v.type(), nInt);
    }

    TEST_F(PrimOpTest, storeDir) {
        auto v = eval("builtins.storeDir");
        ASSERT_THAT(v, IsStringEq(settings.nixStore));
    }

    TEST_F(PrimOpTest, nixVersion) {
        auto v = eval("builtins.nixVersion");
        ASSERT_THAT(v, IsStringEq(nixVersion));
    }

    TEST_F(PrimOpTest, currentSystem) {
        auto v = eval("builtins.currentSystem");
        ASSERT_THAT(v, IsStringEq(evalSettings.getCurrentSystem()));
    }

    TEST_F(PrimOpTest, derivation) {
        auto v = eval("derivation");
        ASSERT_EQ(v.type(), nFunction);
        ASSERT_TRUE(v.isLambda());
        ASSERT_NE(v.payload.lambda.fun, nullptr);
        ASSERT_TRUE(v.payload.lambda.fun->hasFormals());
    }

    TEST_F(PrimOpTest, currentTime) {
        auto v = eval("builtins.currentTime");
        ASSERT_EQ(v.type(), nInt);
        ASSERT_TRUE(v.integer() > 0);
    }

    TEST_F(PrimOpTest, splitVersion) {
        auto v = eval("builtins.splitVersion \"1.2.3git\"");
        ASSERT_THAT(v, IsListOfSize(4));

        const std::vector<std::string_view> strings = { "1", "2", "3", "git" };
        for (const auto [n, p] : enumerate(v.listItems()))
            ASSERT_THAT(*p, IsStringEq(strings[n]));
    }

    class CompareVersionsPrimOpTest :
        public PrimOpTest,
        public testing::WithParamInterface<std::tuple<std::string, const int>>
        {};

    TEST_P(CompareVersionsPrimOpTest, compareVersions) {
        auto [expression, expectation] = GetParam();
        auto v = eval(expression);
        ASSERT_THAT(v, IsIntEq(expectation));
    }

#define CASE(a, b, expected) (std::make_tuple("builtins.compareVersions \"" #a "\" \"" #b "\"", expected))
    INSTANTIATE_TEST_SUITE_P(
            compareVersions,
            CompareVersionsPrimOpTest,
            testing::Values(
                // The first two are weird cases. Intuition tells they should
                // be the same but they aren't.
                CASE(1.0, 1.0.0, -1),
                CASE(1.0.0, 1.0, 1),
                // the following are from the nix-env manual:
                CASE(1.0, 2.3, -1),
                CASE(2.1, 2.3, -1),
                CASE(2.3, 2.3, 0),
                CASE(2.5, 2.3, 1),
                CASE(3.1, 2.3, 1),
                CASE(2.3.1, 2.3, 1),
                CASE(2.3.1, 2.3a, 1),
                CASE(2.3pre1, 2.3, -1),
                CASE(2.3pre3, 2.3pre12, -1),
                CASE(2.3a, 2.3c, -1),
                CASE(2.3pre1, 2.3c, -1),
                CASE(2.3pre1, 2.3q, -1)
            )
    );
#undef CASE


  class ParseDrvNamePrimOpTest :
      public PrimOpTest,
      public testing::WithParamInterface<std::tuple<std::string, std::string_view, std::string_view>>
      {};

    TEST_P(ParseDrvNamePrimOpTest, parseDrvName) {
        auto [input, expectedName, expectedVersion] = GetParam();
        const auto expr = fmt("builtins.parseDrvName \"%1%\"", input);
        auto v = eval(expr);
        ASSERT_THAT(v, IsAttrsOfSize(2));

        auto name = v.attrs()->find(createSymbol("name"));
        ASSERT_TRUE(name);
        ASSERT_THAT(*name->value, IsStringEq(expectedName));

        auto version = v.attrs()->find(createSymbol("version"));
        ASSERT_TRUE(version);
        ASSERT_THAT(*version->value, IsStringEq(expectedVersion));
    }

    INSTANTIATE_TEST_SUITE_P(
            parseDrvName,
            ParseDrvNamePrimOpTest,
            testing::Values(
                std::make_tuple("nix-0.12pre12876", "nix", "0.12pre12876"),
                std::make_tuple("a-b-c-1234pre5+git", "a-b-c", "1234pre5+git")
            )
    );

    TEST_F(PrimOpTest, replaceStrings) {
        // FIXME: add a test that verifies the string context is as expected
        auto v = eval("builtins.replaceStrings [\"oo\" \"a\"] [\"a\" \"i\"] \"foobar\"");
        ASSERT_EQ(v.type(), nString);
        ASSERT_EQ(v.string_view(), "fabir");
    }

    TEST_F(PrimOpTest, concatStringsSep) {
        // FIXME: add a test that verifies the string context is as expected
        auto v = eval("builtins.concatStringsSep \"%\" [\"foo\" \"bar\" \"baz\"]");
        ASSERT_EQ(v.type(), nString);
        ASSERT_EQ(v.string_view(), "foo%bar%baz");
    }

    TEST_F(PrimOpTest, split1) {
        // v = [ "" [ "a" ] "c" ]
        auto v = eval("builtins.split \"(a)b\" \"abc\"");
        ASSERT_THAT(v, IsListOfSize(3));

        ASSERT_THAT(*v.listElems()[0], IsStringEq(""));

        ASSERT_THAT(*v.listElems()[1], IsListOfSize(1));
        ASSERT_THAT(*v.listElems()[1]->listElems()[0], IsStringEq("a"));

        ASSERT_THAT(*v.listElems()[2], IsStringEq("c"));
    }

    TEST_F(PrimOpTest, split2) {
        // v is expected to be a list [ "" [ "a" ] "b" [ "c"] "" ]
        auto v = eval("builtins.split \"([ac])\" \"abc\"");
        ASSERT_THAT(v, IsListOfSize(5));

        ASSERT_THAT(*v.listElems()[0], IsStringEq(""));

        ASSERT_THAT(*v.listElems()[1], IsListOfSize(1));
        ASSERT_THAT(*v.listElems()[1]->listElems()[0], IsStringEq("a"));

        ASSERT_THAT(*v.listElems()[2], IsStringEq("b"));

        ASSERT_THAT(*v.listElems()[3], IsListOfSize(1));
        ASSERT_THAT(*v.listElems()[3]->listElems()[0], IsStringEq("c"));

        ASSERT_THAT(*v.listElems()[4], IsStringEq(""));
    }

    TEST_F(PrimOpTest, split3) {
        auto v = eval("builtins.split \"(a)|(c)\" \"abc\"");
        ASSERT_THAT(v, IsListOfSize(5));

        // First list element
        ASSERT_THAT(*v.listElems()[0], IsStringEq(""));

        // 2nd list element is a list [ "" null ]
        ASSERT_THAT(*v.listElems()[1], IsListOfSize(2));
        ASSERT_THAT(*v.listElems()[1]->listElems()[0], IsStringEq("a"));
        ASSERT_THAT(*v.listElems()[1]->listElems()[1], IsNull());

        // 3rd element
        ASSERT_THAT(*v.listElems()[2], IsStringEq("b"));

        // 4th element is a list: [ null "c" ]
        ASSERT_THAT(*v.listElems()[3], IsListOfSize(2));
        ASSERT_THAT(*v.listElems()[3]->listElems()[0], IsNull());
        ASSERT_THAT(*v.listElems()[3]->listElems()[1], IsStringEq("c"));

        // 5th element is the empty string
        ASSERT_THAT(*v.listElems()[4], IsStringEq(""));
    }

    TEST_F(PrimOpTest, split4) {
        auto v = eval("builtins.split \"([[:upper:]]+)\" \" FOO \"");
        ASSERT_THAT(v, IsListOfSize(3));
        auto first = v.listElems()[0];
        auto second = v.listElems()[1];
        auto third = v.listElems()[2];

        ASSERT_THAT(*first, IsStringEq(" "));

        ASSERT_THAT(*second, IsListOfSize(1));
        ASSERT_THAT(*second->listElems()[0], IsStringEq("FOO"));

        ASSERT_THAT(*third, IsStringEq(" "));
    }

    TEST_F(PrimOpTest, match1) {
        auto v = eval("builtins.match \"ab\" \"abc\"");
        ASSERT_THAT(v, IsNull());
    }

    TEST_F(PrimOpTest, match2) {
        auto v = eval("builtins.match \"abc\" \"abc\"");
        ASSERT_THAT(v, IsListOfSize(0));
    }

    TEST_F(PrimOpTest, match3) {
        auto v = eval("builtins.match \"a(b)(c)\" \"abc\"");
        ASSERT_THAT(v, IsListOfSize(2));
        ASSERT_THAT(*v.listElems()[0], IsStringEq("b"));
        ASSERT_THAT(*v.listElems()[1], IsStringEq("c"));
    }

    TEST_F(PrimOpTest, match4) {
        auto v = eval("builtins.match \"[[:space:]]+([[:upper:]]+)[[:space:]]+\" \"  FOO   \"");
        ASSERT_THAT(v, IsListOfSize(1));
        ASSERT_THAT(*v.listElems()[0], IsStringEq("FOO"));
    }

    TEST_F(PrimOpTest, match5) {
        // The regex "\\{}" is valid and matches the string "{}".
        // Caused a regression before when trying to switch from std::regex to boost::regex.
        // See https://github.com/NixOS/nix/pull/7762#issuecomment-1834303659
        auto v = eval("builtins.match \"\\\\{}\" \"{}\"");
        ASSERT_THAT(v, IsListOfSize(0));
    }

    TEST_F(PrimOpTest, attrNames) {
        auto v = eval("builtins.attrNames { x = 1; y = 2; z = 3; a = 2; }");
        ASSERT_THAT(v, IsListOfSize(4));

        // ensure that the list is sorted
        const std::vector<std::string_view> expected { "a", "x", "y", "z" };
        for (const auto [n, elem] : enumerate(v.listItems()))
            ASSERT_THAT(*elem, IsStringEq(expected[n]));
    }

    TEST_F(PrimOpTest, genericClosure_not_strict) {
        // Operator should not be used when startSet is empty
        auto v = eval("builtins.genericClosure { startSet = []; }");
        ASSERT_THAT(v, IsListOfSize(0));
    }
} /* namespace nix */