// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <cstdint>
#include <memory>
#include <vector>

#include "src/developer/shell/common/ast_builder.h"
#include "src/developer/shell/interpreter/test/interpreter_test.h"

shell::console::AstBuilder::NodeId NullNode;

TEST_F(InterpreterTest, ContextNotCreated) {
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(1));
  Finish(kError);

  ASSERT_EQ("Execution context 1 not defined.\n", GlobalErrors());
}

TEST_F(InterpreterTest, ContextCreatedTwice) {
  ASSERT_CALL_OK(shell().CreateExecutionContext(1));
  ASSERT_CALL_OK(shell().CreateExecutionContext(1));
  Finish(kError);

  ASSERT_EQ("Execution context 1 is already in use.\n", GlobalErrors());
}

TEST_F(InterpreterTest, NoPendingInstruction) {
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));
  Finish(kExecute);

  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::ANALYSIS_ERROR, context->result);

  std::string error_result = context->error_stream.str();
  ASSERT_EQ("No pending instruction to execute.\n", error_result);
}

TEST_F(InterpreterTest, GlobalExpression) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.SetRoot(builder.AddIntegerLiteral(1));

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));

  Finish(kExecute);

  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::ANALYSIS_ERROR, context->result);

  std::string error_result = context->error_stream.str();
  ASSERT_EQ("Node 1:1 can't be a root node.\n", error_result);
}

TEST_F(InterpreterTest, BadAst) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.AddIntegerLiteral(1, true);

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));
  Finish(kExecute);

  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::ANALYSIS_ERROR, context->result);

  std::string error_result = context->error_stream.str();
  ASSERT_EQ("Pending AST nodes for execution context 1.\n", error_result);
}

TEST_F(InterpreterTest, VariableDefinition) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.AddVariableDeclaration("foo", builder.TypeUint64(), NullNode, false, true);
  builder.AddVariableDeclaration("bar", builder.TypeInt64(), builder.AddIntegerLiteral(1, true),
                                 true, true);
  builder.AddVariableDeclaration("x", builder.TypeUint64(), builder.AddIntegerLiteral(10, false),
                                 true, true);

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().DumpExecutionContext(context->id));
  Finish(kDump);

  ASSERT_FALSE(last_text_result_partial());
  ASSERT_EQ(text_results().size(), static_cast<size_t>(3));
  ASSERT_EQ(text_results()[0], "var foo: uint64\n");
  ASSERT_EQ(text_results()[1], "const bar: int64(-1)\n");
  ASSERT_EQ(text_results()[2], "const x: uint64(10)\n");
}

TEST_F(InterpreterTest, BuiltinTypes) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.AddVariableDeclaration("b", builder.TypeBool(), NullNode, false, true);
  builder.AddVariableDeclaration("c", builder.TypeChar(), NullNode, false, true);
  builder.AddVariableDeclaration("s", builder.TypeString(), NullNode, false, true);
  builder.AddVariableDeclaration("i8", builder.TypeInt8(), NullNode, false, true);
  builder.AddVariableDeclaration("u8", builder.TypeUint8(), NullNode, false, true);
  builder.AddVariableDeclaration("i16", builder.TypeInt16(), NullNode, false, true);
  builder.AddVariableDeclaration("u16", builder.TypeUint16(), NullNode, false, true);
  builder.AddVariableDeclaration("i32", builder.TypeInt32(), NullNode, false, true);
  builder.AddVariableDeclaration("u32", builder.TypeUint32(), NullNode, false, true);
  builder.AddVariableDeclaration("i64", builder.TypeInt64(), NullNode, false, true);
  builder.AddVariableDeclaration("u64", builder.TypeUint64(), NullNode, false, true);
  builder.AddVariableDeclaration("big_int", builder.TypeInteger(), NullNode, false, true);
  builder.AddVariableDeclaration("f32", builder.TypeFloat32(), NullNode, false, true);
  builder.AddVariableDeclaration("f64", builder.TypeFloat64(), NullNode, false, true);

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().DumpExecutionContext(context->id));
  Finish(kDump);

  ASSERT_FALSE(last_text_result_partial());
  ASSERT_EQ(text_results().size(), static_cast<size_t>(14));
  ASSERT_EQ(text_results()[0], "var b: bool\n");
  ASSERT_EQ(text_results()[1], "var c: char\n");
  ASSERT_EQ(text_results()[2], "var s: string\n");
  ASSERT_EQ(text_results()[3], "var i8: int8\n");
  ASSERT_EQ(text_results()[4], "var u8: uint8\n");
  ASSERT_EQ(text_results()[5], "var i16: int16\n");
  ASSERT_EQ(text_results()[6], "var u16: uint16\n");
  ASSERT_EQ(text_results()[7], "var i32: int32\n");
  ASSERT_EQ(text_results()[8], "var u32: uint32\n");
  ASSERT_EQ(text_results()[9], "var i64: int64\n");
  ASSERT_EQ(text_results()[10], "var u64: uint64\n");
  ASSERT_EQ(text_results()[11], "var big_int: integer\n");
  ASSERT_EQ(text_results()[12], "var f32: float32\n");
  ASSERT_EQ(text_results()[13], "var f64: float64\n");
}

TEST_F(InterpreterTest, Objects) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));
  shell::console::AstBuilder builder(kFileId);

  {
    std::vector<std::string> names;
    std::vector<shell::console::AstBuilder::NodeId> values;
    std::vector<fuchsia_shell::wire::ShellType> types;
    shell::console::AstBuilder::NodePair object_pair =
        AddObject(builder, names, values, std::move(types));
    builder.AddVariableDeclaration("obj1", builder.TypeObject(object_pair.schema_node),
                                   object_pair.value_node, /*is_const=*/false, /*is_root=*/true);
  }

  {
    std::vector<std::string> names{"alpha", "beta"};
    std::vector<shell::console::AstBuilder::NodeId> values{builder.AddIntegerLiteral(4, false),
                                                           builder.AddIntegerLiteral(5, false)};
    std::vector<fuchsia_shell::wire::ShellType> types;
    types.emplace_back(builder.TypeUint64());
    types.emplace_back(builder.TypeUint64());
    shell::console::AstBuilder::NodePair object_pair =
        AddObject(builder, names, values, std::move(types));
    builder.AddVariableDeclaration("obj2", builder.TypeObject(object_pair.schema_node),
                                   object_pair.value_node, /*is_const=*/false, /*is_root=*/true);
  }
  {
    std::vector<std::string> names{"alpha", "beta"};
    std::vector<shell::console::AstBuilder::NodeId> values{builder.AddStringLiteral("Hello"),
                                                           builder.AddStringLiteral("world!")};
    std::vector<fuchsia_shell::wire::ShellType> types;
    types.emplace_back(builder.TypeString());
    types.emplace_back(builder.TypeString());
    shell::console::AstBuilder::NodePair object_pair =
        AddObject(builder, names, values, std::move(types));
    builder.AddVariableDeclaration("obj3", builder.TypeObject(object_pair.schema_node),
                                   object_pair.value_node, /*is_const=*/false, /*is_root=*/true);
  }
  {
    std::vector<std::string> inner_names{"alpha", "beta"};
    std::vector<shell::console::AstBuilder::NodeId> inner_values{
        builder.AddStringLiteral("Hello"), builder.AddStringLiteral("world!")};
    std::vector<fuchsia_shell::wire::ShellType> inner_types;
    inner_types.emplace_back(builder.TypeString());
    inner_types.emplace_back(builder.TypeString());
    shell::console::AstBuilder::NodePair inner_object_pair =
        AddObject(builder, inner_names, inner_values, std::move(inner_types));
    std::vector<std::string> outer_names{"inner", "extra"};
    std::vector<shell::console::AstBuilder::NodeId> outer_values{
        inner_object_pair.value_node, builder.AddStringLiteral("Extra value")};
    std::vector<fuchsia_shell::wire::ShellType> outer_types;
    outer_types.emplace_back(builder.TypeObject(inner_object_pair.schema_node));
    outer_types.emplace_back(builder.TypeString());
    shell::console::AstBuilder::NodePair outer_object_pair =
        AddObject(builder, outer_names, outer_values, std::move(outer_types));
    builder.AddVariableDeclaration("obj4", builder.TypeObject(outer_object_pair.schema_node),
                                   outer_object_pair.value_node, /*is_const=*/false,
                                   /*is_root=*/true);
  }

  builder.AddEmitResult(builder.AddVariable("obj1"));
  builder.AddEmitResult(builder.AddVariable("obj2"));
  builder.AddEmitResult(builder.AddVariable("obj3"));
  builder.AddEmitResult(builder.AddVariable("obj4"));

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));
  Finish(kExecute);

  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::OK, context->GetResult());

  CHECK_RESULT(0, "{}");
  CHECK_RESULT(1, "{alpha: uint64(4), beta: uint64(5)}");
  CHECK_RESULT(2, "{alpha: string(\"Hello\"), beta: string(\"world!\")}");
  CHECK_RESULT(3,
               "{inner: {alpha: string(\"Hello\"), beta: string(\"world!\")},"
               " extra: string(\"Extra value\")}");
}

TEST_F(InterpreterTest, VariableOk) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.AddVariableDeclaration("foo", builder.TypeUint64(), builder.AddIntegerLiteral(1, false),
                                 false, true);
  builder.AddVariableDeclaration("bar", builder.TypeUint64(), builder.AddIntegerLiteral(10, false),
                                 false, true);
  builder.AddVariableDeclaration("groucho", builder.TypeString(),
                                 builder.AddStringLiteral("A Marx brother"), false, true);

  builder.AddEmitResult(builder.AddVariable("foo"));
  builder.AddEmitResult(builder.AddVariable("bar"));
  builder.AddEmitResult(builder.AddVariable("groucho"));

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));
  Finish(kExecute);

  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::OK, context->GetResult());

  CHECK_RESULT(0, "1");
  CHECK_RESULT(1, "10");
  CHECK_RESULT(2, "\"A Marx brother\"");
}

TEST_F(InterpreterTest, VariableNoType) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.AddVariableDeclaration("bar", builder.TypeUndef(), NullNode, false, true);
  builder.AddVariableDeclaration("foo", builder.TypeUndef(), builder.AddIntegerLiteral(1, false),
                                 false, true);

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));

  Finish(kExecute);
  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::ANALYSIS_ERROR, context->result);

  std::string error_result = context->error_stream.str();
  ASSERT_EQ(
      "node 1:1 Type not defined.\n"
      "node 1:3 Type not defined.\n",
      error_result);
}

TEST_F(InterpreterTest, VariableDefinedTwice) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.AddVariableDeclaration("bar", builder.TypeUint64(), NullNode, false, true);
  builder.AddVariableDeclaration("bar", builder.TypeUint64(), NullNode, false, true);

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));

  Finish(kExecute);
  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::ANALYSIS_ERROR, context->result);

  std::string error_result = context->error_stream.str();
  ASSERT_EQ(
      "node 1:2 Variable 'bar' already defined.\n"
      "node 1:1 First definition.\n",
      error_result);
}

TEST_F(InterpreterTest, BadIntegerLiterals) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.AddVariableDeclaration("i8p", builder.TypeInt8(), builder.AddIntegerLiteral(0x80, false),
                                 true, true);
  builder.AddVariableDeclaration("i8n", builder.TypeInt8(), builder.AddIntegerLiteral(0x81, true),
                                 true, true);
  builder.AddVariableDeclaration("u8p", builder.TypeUint8(),
                                 builder.AddIntegerLiteral(0x100, false), true, true);
  builder.AddVariableDeclaration("u8n", builder.TypeUint8(), builder.AddIntegerLiteral(1, true),
                                 true, true);

  builder.AddVariableDeclaration("i16p", builder.TypeInt16(),
                                 builder.AddIntegerLiteral(0x8000, false), true, true);
  builder.AddVariableDeclaration("i16n", builder.TypeInt16(),
                                 builder.AddIntegerLiteral(0x8001, true), true, true);
  builder.AddVariableDeclaration("u16p", builder.TypeUint16(),
                                 builder.AddIntegerLiteral(0x10000, false), true, true);
  builder.AddVariableDeclaration("u16n", builder.TypeUint16(), builder.AddIntegerLiteral(1, true),
                                 true, true);

  builder.AddVariableDeclaration("i32p", builder.TypeInt32(),
                                 builder.AddIntegerLiteral(0x80000000, false), true, true);
  builder.AddVariableDeclaration("i32n", builder.TypeInt32(),
                                 builder.AddIntegerLiteral(0x80000001, true), true, true);
  builder.AddVariableDeclaration("u32p", builder.TypeUint32(),
                                 builder.AddIntegerLiteral(0x100000000L, false), true, true);
  builder.AddVariableDeclaration("u32n", builder.TypeUint32(), builder.AddIntegerLiteral(1, true),
                                 true, true);

  builder.AddVariableDeclaration("i64p", builder.TypeInt64(),
                                 builder.AddIntegerLiteral(0x8000000000000000UL, false), true,
                                 true);
  builder.AddVariableDeclaration("i64n", builder.TypeInt64(),
                                 builder.AddIntegerLiteral(0x8000000000000001UL, true), true, true);
  builder.AddVariableDeclaration("u64n", builder.TypeUint64(), builder.AddIntegerLiteral(1, true),
                                 true, true);

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));

  Finish(kExecute);
  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::ANALYSIS_ERROR, context->result);

  std::string error_result = context->error_stream.str();
  ASSERT_EQ(
      "node 1:1 Can't create an integer literal of type int8 with 128.\n"
      "node 1:3 Can't create an integer literal of type int8 with -129.\n"
      "node 1:5 Can't create an integer literal of type uint8 with 256.\n"
      "node 1:7 Can't create an integer literal of type uint8 with -1.\n"
      "node 1:9 Can't create an integer literal of type int16 with 32768.\n"
      "node 1:11 Can't create an integer literal of type int16 with -32769.\n"
      "node 1:13 Can't create an integer literal of type uint16 with 65536.\n"
      "node 1:15 Can't create an integer literal of type uint16 with -1.\n"
      "node 1:17 Can't create an integer literal of type int32 with 2147483648.\n"
      "node 1:19 Can't create an integer literal of type int32 with -2147483649.\n"
      "node 1:21 Can't create an integer literal of type uint32 with 4294967296.\n"
      "node 1:23 Can't create an integer literal of type uint32 with -1.\n"
      "node 1:25 Can't create an integer literal of type int64 with 9223372036854775808.\n"
      "node 1:27 Can't create an integer literal of type int64 with -9223372036854775809.\n"
      "node 1:29 Can't create an integer literal of type uint64 with -1.\n",
      error_result);
}

TEST_F(InterpreterTest, GoodIntegerLiterals) {
  constexpr uint64_t kFileId = 1;
  InterpreterTestContext* context = CreateContext();
  ASSERT_CALL_OK(shell().CreateExecutionContext(context->id));

  shell::console::AstBuilder builder(kFileId);
  builder.AddVariableDeclaration("i8p", builder.TypeInt8(), builder.AddIntegerLiteral(0x7f, false),
                                 true, true);
  builder.AddVariableDeclaration("i8n", builder.TypeInt8(), builder.AddIntegerLiteral(0x80, true),
                                 true, true);
  builder.AddVariableDeclaration("u8p", builder.TypeUint8(), builder.AddIntegerLiteral(0xff, false),
                                 true, true);
  builder.AddVariableDeclaration("u8n", builder.TypeUint8(), builder.AddIntegerLiteral(0, false),
                                 true, true);

  builder.AddVariableDeclaration("i16p", builder.TypeInt16(),
                                 builder.AddIntegerLiteral(0x7fff, false), true, true);
  builder.AddVariableDeclaration("i16n", builder.TypeInt16(),
                                 builder.AddIntegerLiteral(0x8000, true), true, true);
  builder.AddVariableDeclaration("u16p", builder.TypeUint16(),
                                 builder.AddIntegerLiteral(0xffff, false), true, true);
  builder.AddVariableDeclaration("u16n", builder.TypeUint16(), builder.AddIntegerLiteral(0, false),
                                 true, true);

  builder.AddVariableDeclaration("i32p", builder.TypeInt32(),
                                 builder.AddIntegerLiteral(0x7fffffff, false), true, true);
  builder.AddVariableDeclaration("i32n", builder.TypeInt32(),
                                 builder.AddIntegerLiteral(0x80000000, true), true, true);
  builder.AddVariableDeclaration("u32p", builder.TypeUint32(),
                                 builder.AddIntegerLiteral(0xffffffffL, false), true, true);
  builder.AddVariableDeclaration("u32n", builder.TypeUint32(), builder.AddIntegerLiteral(0, false),
                                 true, true);

  builder.AddVariableDeclaration("i64p", builder.TypeInt64(),
                                 builder.AddIntegerLiteral(0x7fffffffffffffffUL, false), true,
                                 true);
  builder.AddVariableDeclaration("i64n", builder.TypeInt64(),
                                 builder.AddIntegerLiteral(0x8000000000000000UL, true), true, true);
  builder.AddVariableDeclaration("u64p", builder.TypeUint64(),
                                 builder.AddIntegerLiteral(0xffffffffffffffffUL, false), true,
                                 true);
  builder.AddVariableDeclaration("u64n", builder.TypeUint64(), builder.AddIntegerLiteral(0, false),
                                 true, true);

  ASSERT_CALL_OK(shell().AddNodes(context->id, builder.DefsAsVectorView()));
  ASSERT_CALL_OK(shell().ExecuteExecutionContext(context->id));
  Finish(kExecute);

  ASSERT_EQ(fuchsia_shell::wire::ExecuteResult::OK, context->GetResult());
}
