require "../../spec_helper"

private def expect_augment(before : String, after : String, *, file : String = __FILE__, line : Int32 = __LINE__)
  result = semantic(before)
  result.node.to_s.chomp.should eq(after.chomp), file: file, line: line
end

private def expect_no_augment(code : String, flags = nil)
  result = semantic(code, flags: flags)
  result.node.to_s.chomp.should eq(code.chomp)
end

private def it_augments_for_ivar(ivar_type : String, expected_type : String, file = __FILE__, line = __LINE__)
  it "augments #{ivar_type}", file, line do
    before = <<-CRYSTAL
      class Foo
        @x : #{ivar_type}
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class Foo
        @x : #{ivar_type}
        def initialize(value : #{expected_type})
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after, file: file, line: line
  end
end

describe "Semantic: restrictions augmenter" do
  it_augments_for_ivar "Nil", "::Nil"
  it_augments_for_ivar "Bool", "::Bool"
  it_augments_for_ivar "Char", "::Char"
  it_augments_for_ivar "Int32", "::Int32"
  it_augments_for_ivar "Float32", "::Float32"
  it_augments_for_ivar "Symbol", "::Symbol"
  it_augments_for_ivar "String", "::String"
  it_augments_for_ivar "Array(String)", "::Array(::String)"
  it_augments_for_ivar "Tuple(Int32, Char)", "::Tuple(::Int32, ::Char)"
  it_augments_for_ivar "NamedTuple(a: Int32, b: Char)", "::NamedTuple(a: ::Int32, b: ::Char)"
  it_augments_for_ivar "Proc(Int32, Char)", "::Int32 -> ::Char"
  it_augments_for_ivar "Proc(Int32, Nil)", "::Int32 -> _"
  it_augments_for_ivar "Pointer(Void)", "::Pointer(::Void)"
  it_augments_for_ivar "StaticArray(Int32, 8)", "::StaticArray(::Int32, 8)"
  it_augments_for_ivar "Char | Int32 | String", "::Char | ::Int32 | ::String"
  it_augments_for_ivar "Char | Int32 | String", "::Char | ::Int32 | ::String"
  it_augments_for_ivar "Int32.class", "::Int32.class"
  it_augments_for_ivar "NoReturn", "::NoReturn"
  it_augments_for_ivar "Array(Int32).class", "::Array(::Int32).class"
  it_augments_for_ivar "Enumerable(Int32).class", "::Enumerable(::Int32).class"

  it "augments relative public type" do
    before = <<-CRYSTAL
      class Foo
        class Bar
          class Baz
          end
        end

        @x : Bar:: Baz

        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class Foo
        class Bar
          class Baz
          end
        end

        @x : Bar::Baz

        def initialize(value : ::Foo::Bar::Baz)
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments relative private type" do
    before = <<-CRYSTAL
      class Foo
        private class Bar
          class Baz
          end
        end

        @x : Bar:: Baz

        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class Foo
        private class Bar
          class Baz
          end
        end

        @x : Bar::Baz

        def initialize(value : Bar::Baz)
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments relative private type in same namespace" do
    before = <<-CRYSTAL
      class Foo
        private class Bar
        end
        private class Baz
          @x : Bar
          def initialize(value)
            @x = value
          end
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class Foo
        private class Bar
        end
        private class Baz
          @x : Bar
          def initialize(value : Bar)
            @x = value
          end
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments generic uninstantiated type" do
    before = <<-CRYSTAL
      class Foo(T)
        @x : Array(T)
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class Foo(T)
        @x : Array(T)
        def initialize(value : ::Array(T))
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments for class var" do
    before = <<-CRYSTAL
      class Foo
        @@x = 1
        def self.set(value)
          @@x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class Foo
        @@x = 1
        def self.set(value : ::Int32)
          @@x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "doesn't augment if assigned inside if" do
    expect_no_augment <<-CRYSTAL
      class Foo
        @x : Int32
        def initialize(value)
          if value
            @x = value
          end
        end
      end
      CRYSTAL
  end

  it "doesn't augment if assigned inside while" do
    expect_no_augment <<-CRYSTAL
      class Foo
        @x : Int32
        def initialize(value)
          while false
            @x = value
          end
        end
      end
      CRYSTAL
  end

  it "doesn't augment if assigned inside block" do
    expect_no_augment <<-CRYSTAL
      def foo(&)
        yield
      end
      class Foo
        @x : Int32
        def initialize(value)
          foo do
            @x = value
          end
        end
      end
      CRYSTAL
  end

  it "doesn't augment if the no_restrictions_augmenter flag is present" do
    expect_no_augment <<-CRYSTAL, flags: "no_restrictions_augmenter"
      class Foo
        @x : Int32
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL
  end

  it "augments recursive alias type (#12134)" do
    before = <<-CRYSTAL
      alias BasicObject = Array(BasicObject) | Hash(String, BasicObject)
      class Foo
        def initialize(value = Hash(String, BasicObject).new)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      alias BasicObject = Array(BasicObject) | Hash(String, BasicObject)
      class Foo
        def initialize(value : ::Hash(::String, ::BasicObject) = Hash(String, BasicObject).new)
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments typedef" do
    before = <<-CRYSTAL
      lib LibFoo
        type X = Int32
      end
      class Foo
        @x : LibFoo::X
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      lib LibFoo
        type X = Int32
      end
      class Foo
        @x : LibFoo::X
        def initialize(value : ::LibFoo::X)
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments virtual type" do
    before = <<-CRYSTAL
      class A
      end
      class B < A
      end
      class Foo
        @x : A
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class A
      end
      class B < A
      end
      class Foo
        @x : A
        def initialize(value : ::A)
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments virtual metaclass type" do
    before = <<-CRYSTAL
      class A
      end
      class B < A
      end
      class Foo
        @x : A.class
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class A
      end
      class B < A
      end
      class Foo
        @x : A.class
        def initialize(value : ::A.class)
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments type splat" do
    before = <<-CRYSTAL
      class Foo(T)
        @x : Array(*T)
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class Foo(T)
        @x : Array(*T)
        def initialize(value : ::Array(*T))
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "doesn't crash on macro that yields and defines class (#12142)" do
    before = <<-CRYSTAL
      macro foo
        {{yield}}
      end
      foo do
        class Foo
        end
      end
      class Bar
        @x : Foo
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      macro foo
        {{ yield }}
      end

      class Foo
      end

      class Bar
        @x : Foo
        def initialize(value : ::Foo)
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end

  it "augments for Union(*T) (#12435)" do
    before = <<-CRYSTAL
      class Foo(*T)
        @x : Union(*T)
        def initialize(value)
          @x = value
        end
      end
      CRYSTAL

    after = <<-CRYSTAL
      class Foo(*T)
        @x : Union(*T)
        def initialize(value : ::Union(*T))
          @x = value
        end
      end
      CRYSTAL

    expect_augment before, after
  end
end
