require "../../spec_helper"

private STABLE_ABI_TYPES = [
  Nil, Void, Int32, Float32, Bool, Char, Symbol, Pointer(Void),           # primitive scalars
  StaticArray(Int32, 1), Tuple(Int32), NamedTuple(x: Int32), Proc(Int32), # primitive aggregates
  Reference, String,
  Union(Int32, Char), Union(Int32, String), Union(Nil, String), Union(Nil, String, Regex), Union(Nil, Proc(Int32)), # unions
]

private UNSTABLE_ABI_TYPES = [
  Value, Struct, Int, Float,
  Slice, Slice(Int32), Enumerable, Enumerable(Int32), Flags,
  StaticArray(Slice(Int32), 1), Tuple(Slice(Int32)), NamedTuple(x: Slice(Int32)),
  Union(Slice(Int32), Nil),
]

describe "MacroExpander" do
  it "expands simple macro" do
    assert_macro "1 + 2", "1 + 2"
  end

  it "expands macro with string substitution" do
    assert_macro "{{x}}", %("hello"), {x: "hello".string}
  end

  it "expands macro with symbol substitution" do
    assert_macro "{{x}}", ":hello", {x: "hello".symbol}
  end

  it "expands macro with argument-less call substitution" do
    assert_macro "{{x}}", "hello", {x: "hello".call}
  end

  it "expands macro with boolean" do
    assert_macro "{{true}}", "true"
  end

  it "expands macro with integer" do
    assert_macro "{{1}}", "1"
  end

  it "expands macro with char" do
    assert_macro "{{'a'}}", "'a'"
  end

  it "expands macro with string" do
    assert_macro %({{"hello"}}), %("hello")
  end

  it "expands macro with symbol" do
    assert_macro %({{:foo}}), %(:foo)
  end

  it "expands macro with nil" do
    assert_macro %({{nil}}), %(nil)
  end

  it "expands macro with array" do
    assert_macro %({{[1, 2, 3]}}), %([1, 2, 3])
  end

  it "expands macro with hash" do
    assert_macro %({{{:a => 1, :b => 2}}}), "{:a => 1, :b => 2}"
  end

  it "expands macro with tuple" do
    assert_macro %({{{1, 2, 3}}}), %({1, 2, 3})
  end

  it "expands macro with empty tuple" do
    assert_macro "{{x}}", "::Tuple.new", {x: TupleLiteral.new([] of ASTNode)}
  end

  it "expands macro with empty named tuple" do
    assert_macro "{{x}}", "::NamedTuple.new", {x: NamedTupleLiteral.new([] of NamedTupleLiteral::Entry)}
  end

  it "expands macro with range" do
    assert_macro %({{1..3}}), %(1..3)
  end

  it "expands macro with string interpolation" do
    assert_macro "{{ \"hello\#{1 == 1}world\" }}", %("hellotrueworld")
  end

  it "expands macro with var substitution" do
    assert_macro "{{x}}", "hello", {x: "hello".var}
  end

  it "expands macro with or (1)" do
    assert_macro "{{x || 1}}", "1", {x: NilLiteral.new}
  end

  it "expands macro with or (2)" do
    assert_macro "{{x || 1}}", "hello", {x: "hello".var}
  end

  it "expands macro with and (1)" do
    assert_macro "{{x && 1}}", "nil", {x: NilLiteral.new}
  end

  it "expands macro with and (2)" do
    assert_macro "{{x && 1}}", "1", {x: "hello".var}
  end

  describe "if" do
    it "expands macro with if when truthy" do
      assert_macro "{%if true%}hello{%end%}", "hello"
    end

    it "expands macro with if when falsey" do
      assert_macro "{%if false%}hello{%end%}", ""
    end

    it "expands macro with if else when falsey" do
      assert_macro "{%if false%}hello{%else%}bye{%end%}", "bye"
    end

    it "expands macro with if with nop" do
      assert_macro "{%if x%}hello{%else%}bye{%end%}", "bye", {x: Nop.new}
    end

    it "expands macro with if with not" do
      assert_macro "{%if !true%}hello{%else%}bye{%end%}", "bye"
    end
  end

  describe "for" do
    it "expands macro with for over array literal" do
      assert_macro "{%for e in x %}{{e}}{%end%}", "helloworld", {x: ArrayLiteral.new(["hello".var, "world".var] of ASTNode)}
    end

    it "expands macro with for over array literal with index" do
      assert_macro "{%for e, i in x%}{{e}}{{i}}{%end%}", "hello0world1", {x: ArrayLiteral.new(["hello".var, "world".var] of ASTNode)}
    end

    it "expands macro with for over embedded array literal" do
      assert_macro "{%for e in [1, 2]%}{{e}}{%end%}", "12"
    end

    it "expands macro with for over hash literal" do
      assert_macro "{%for k, v in x%}{{k}}{{v}}{%end%}", "acbd", {x: HashLiteral.new([HashLiteral::Entry.new("a".var, "c".var), HashLiteral::Entry.new("b".var, "d".var)])}
    end

    it "expands macro with for over hash literal with index" do
      assert_macro "{%for k, v, i in x%}{{k}}{{v}}{{i}}{%end%}", "ac0bd1", {x: HashLiteral.new([HashLiteral::Entry.new("a".var, "c".var), HashLiteral::Entry.new("b".var, "d".var)])}
    end

    it "expands macro with for over tuple literal" do
      assert_macro "{%for e, i in x%}{{e}}{{i}}{%end%}", "a0b1", {x: TupleLiteral.new(["a".var, "b".var] of ASTNode)}
    end

    it "expands macro with for over range literal" do
      assert_macro "{%for e in 1..3 %}{{e}}{%end%}", "123"
    end

    it "expands macro with for over range literal, evaluating elements" do
      assert_macro "{%for e in x..y %}{{e}}{%end%}", "3456", {x: 3.int32, y: 6.int32}
    end

    it "expands macro with for over range literal, evaluating elements (exclusive)" do
      assert_macro "{%for e in x...y %}{{e}}{%end%}", "345", {x: 3.int32, y: 6.int32}
    end
  end

  it "does regular if" do
    assert_macro %({{1 == 2 ? 3 : 4}}), "4"
  end

  it "does regular unless" do
    assert_macro %({{unless 1 == 2; 3; else; 4; end}}), "3"
  end

  it "does not expand when macro expression is {% ... %}" do
    assert_macro %({% 1 %}), ""
  end

  it "can't use `yield` outside a macro" do
    assert_error %({{yield}}), "can't use `{{yield}}` outside a macro"
  end

  it "outputs invisible location pragmas" do
    node = 42.int32
    node.location = Location.new "foo.cr", 10, 20
    assert_macro %({{node}}), "42", {node: node}, expected_pragmas: {
      0 => [
        Lexer::LocPushPragma.new,
        Lexer::LocSetPragma.new("foo.cr", 10, 20),
      ] of Lexer::LocPragma,
      2 => [
        Lexer::LocPopPragma.new,
      ] of Lexer::LocPragma,
    }
  end

  {% for op in ["sizeof".id, "alignof".id] %}
    describe "{{ op }}" do
      {% for type in STABLE_ABI_TYPES %}
        it "gets {{ op }} {{ type.id }}" do
          # we are not interested in the actual sizes or alignments here, these
          # values are up to the codegen spec suite
          assert_macro %(\{{ {{ op }}({{ type.id }}).is_a?(NumberLiteral) }}), "true"
        end
      {% end %}

      it "gets {{ op }} enum" do
        assert_macro("\{{ {{ op }}(Foo) == {{ op }}(Int16) }}", "true") do |program|
          program.types["Foo"] = EnumType.new(program, program, "Foo", program.int16)
          nil
        end
      end

      it "gets {{ op }} alias" do
        assert_macro("\{{ {{ op }}(Foo) == {{ op }}(Int16) }}", "true") do |program|
          program.types["Foo"] = AliasType.new(program, program, "Foo", Crystal::Path.global("Int16"))
          nil
        end
      end

      it "gets {{ op }} typedef" do
        assert_macro("\{{ {{ op }}(Foo) == {{ op }}(Int16) }}", "true") do |program|
          program.types["Foo"] = TypeDefType.new(program, program, "Foo", program.int16)
          nil
        end
      end

      it "errors with typeof" do
        assert_error %(\{{ {{ op }}(typeof(1)) }})
      end

      {% for type in UNSTABLE_ABI_TYPES %}
        it "errors with {{ type.id }}" do
          assert_error %(\{{ {{ op }}({{ type.id }}) }}), "argument to `{{ op }}` inside macros must be a type with a stable {{ op == "sizeof" ? "size".id : "alignment".id }}"
        end
      {% end %}

      it "errors with alias of unstable type" do
        assert_error <<-CRYSTAL, "argument to `{{ op }}` inside macros must be a type with a stable {{ op == "sizeof" ? "size".id : "alignment".id }}"
          struct Foo
          end

          alias Bar = Foo

          \{{ {{ op }}(Bar) }}
          CRYSTAL
      end

      it "errors with typedef of unstable type" do
        assert_error <<-CRYSTAL, "argument to `{{ op }}` inside macros must be a type with a stable {{ op == "sizeof" ? "size".id : "alignment".id }}"
          lib Lib
            struct Foo
              x : Int32
            end

            type Bar = Foo
          end

          \{{ {{ op }}(Lib::Bar) }}
          CRYSTAL
      end
    end
  {% end %}
end
