require "../../spec_helper"

describe "Semantic: lib" do
  it "types a varargs external" do
    assert_type("lib LibFoo; fun bar(x : Int32, ...) : Int32; end; LibFoo.bar(1, 1.5, 'a')") { int32 }
  end

  it "raises on undefined fun" do
    assert_error %(
      lib LibC
      end

      LibC.foo
      ),
      "undefined fun 'foo' for LibC"
  end

  it "raises wrong number of arguments" do
    assert_error %(
      lib LibC
        fun foo : Int32
      end

      LibC.foo 1
      ),
      "wrong number of arguments for 'LibC#foo' (given 1, expected 0)"
  end

  it "raises wrong argument type" do
    assert_error %(
      lib LibC
        fun foo(x : Int32) : Int32
      end

      LibC.foo 'a'
      ),
      "argument 'x' of 'LibC#foo' must be Int32, not Char"
  end

  it "reports error when changing var type and something breaks" do
    assert_error %(
      class LibFoo
        def initialize
          @value = 1
        end

        def value
          @value
        end

        def value=(@value : Char)
        end
      end

      f = LibFoo.new
      f.value + 1
      f.value = 'a'
      ),
      "undefined method '+' for Char"
  end

  it "reports error when changing instance var type and something breaks" do
    assert_error "
      lib Lib
        fun bar(c : Char)
      end

      class Foo
        def value=(@value : Int32 | Char)
        end

        def value
          @value
        end
      end

      def foo(x)
        x.value = 'a'
        Lib.bar x.value
      end

      f = Foo.new
      foo(f)

      f.value = 1
      ",
      "argument 'c' of 'Lib#bar' must be Char"
  end

  it "reports error on fun argument type not primitive like" do
    assert_error "lib LibFoo; fun foo(x : Reference); end",
      "only primitive types"
  end

  it "reports error on fun argument type not primitive like, Nil (#2994)" do
    assert_error "lib LibFoo; fun foo(x : Nil); end",
      "only primitive types"
  end

  it "reports error on fun return type not primitive like" do
    assert_error "lib LibFoo; fun foo : Reference; end",
      "only primitive types"
  end

  it "reports error on struct field type not primitive like" do
    assert_error "lib LibFoo; struct Foo; x : Reference; end; end",
      "only primitive types"
  end

  it "reports error on typedef type not primitive like" do
    assert_error "lib LibFoo; type Foo = Reference; end",
      "only primitive types"
  end

  it "reports error out can only be used with lib funs" do
    assert_error "foo(out x)",
      "out can only be used with lib funs"
  end

  it "reports error out can only be used with lib funs in named argument" do
    assert_error "foo(x: out x)",
      "out can only be used with lib funs"
  end

  it "reports error if using out with an already declared variable" do
    assert_error %(
      lib Lib
        fun foo(x : Int32*)
      end

      x = Pointer(Int32).malloc(1_u64)
      Lib.foo out x
      ),
      "variable 'x' is already defined, `out` must be used to define a variable, use another name", inject_primitives: true
  end

  it "allows invoking out with underscore " do
    assert_type(%(
      lib Lib
        fun foo(x : Int32*) : Float64
      end

      Lib.foo out _
      )) { float64 }
  end

  it "reports redefinition of fun with different signature" do
    assert_error "
      lib LibC
        fun foo : Int32
        fun foo : Int64
      end
      ",
      "fun redefinition with different signature"
  end

  it "types lib var get" do
    assert_type("
      lib LibC
        $errno : Int32
      end

      LibC.errno
      ") { int32 }
  end

  it "types lib var set" do
    assert_type("
      lib LibC
        $errno : Int32
      end

      LibC.errno = 1
      ") { int32 }
  end

  it "types lib var get with forward declaration" do
    assert_type("
      lib LibC
        $errno : A

        alias A = Int32
      end

      LibC.errno
      ") { int32 }
  end

  it "defined fun with aliased type" do
    assert_type("
      lib LibC
        alias SizeT = Int32
        fun foo(x : SizeT) : SizeT
      end

      LibC.foo(1)
      ") { int32 }
  end

  it "overrides definition of fun" do
    result = assert_type("
      lib LibC
        fun foo(x : Int32) : Float64
      end

      lib LibC
        fun foo = bar(x : Int32) : Float64
      end

      LibC.foo(1)
      ") { float64 }
    mod = result.program
    lib_type = mod.types["LibC"].as(LibType)
    foo = lib_type.lookup_first_def("foo", false).as(External)
    foo.real_name.should eq("bar")
  end

  it "error if passing type to LibC with to_unsafe but type doesn't match" do
    assert_error "
      lib LibC
        fun foo(x : Int32) : Int32
      end

      class Foo
        def to_unsafe
          'a'
        end
      end

      LibC.foo Foo.new
      ", "argument 'x' of 'LibC#foo' must be Int32, not Foo (nor Char returned by 'Foo#to_unsafe')"
  end

  it "error if passing non primitive type as varargs" do
    assert_error "
      lib LibC
        fun foo(x : Int32, ...)
      end

      class Foo
      end

      LibC.foo 1, Foo.new
      ", "argument #2 of 'LibC#foo' is not a primitive type and no Foo#to_unsafe method found"
  end

  it "error if passing non primitive type as varargs invoking to_unsafe" do
    assert_error "
      lib LibC
        fun foo(x : Int32, ...)
      end

      class Bar
      end

      class Foo
        def to_unsafe
          Bar.new
        end
      end

      LibC.foo 1, Foo.new
      ", "converted Foo invoking to_unsafe, but Bar is not a primitive type"
  end

  it "allows passing splat to LibC fun" do
    assert_type(%(
      lib LibC
        fun foo(x : Int32, y : Float64, ...) : Float64
      end

      t = {1, 2.5, 3, 4}
      LibC.foo *t
      )) { float64 }
  end

  it "allows passing double splat to LibC fun" do
    assert_type(%(
      lib LibC
        fun foo(x : Int32, y : Float64) : Float64
      end

      t = {y: 2.5, x: 3}
      LibC.foo **t
      ), inject_primitives: true) { float64 }
  end

  it "errors if missing link arguments" do
    assert_error %(
      @[Link]
      lib LibFoo
      end
      ),
      "missing link arguments: must at least specify a library name"
  end

  it "errors if first argument is not a string" do
    assert_error %(
      @[Link(1)]
      lib LibFoo
      end
      ),
      "'lib' link argument must be a String"
  end

  it "errors if second argument is not a string" do
    assert_error %(
      @[Link("foo", 1)]
      lib LibFoo
      end
      ),
      "'ldflags' link argument must be a String"
  end

  it "errors if third argument is not a bool" do
    assert_error %(
      @[Link("foo", "bar", 1)]
      lib LibFoo
      end
      ),
      "'static' link argument must be a Bool"
  end

  it "errors if fourth argument is not a bool" do
    assert_error %(
      @[Link("foo", "bar", true, 1)]
      lib LibFoo
      end
      ),
      "'framework' link argument must be a String"
  end

  it "errors if too many link arguments" do
    assert_error %(
      @[Link("foo", "bar", true, "Cocoa", 1)]
      lib LibFoo
      end
      ),
      "wrong number of link arguments (given 5, expected 1..4)"
  end

  it "errors if unknown named arg" do
    assert_error %(
      @[Link(boo: "bar")]
      lib LibFoo
      end
      ),
      "unknown link argument: 'boo' (valid arguments are 'lib', 'ldflags', 'static', 'pkg_config', 'framework', 'wasm_import_module', and 'dll')"
  end

  it "allows dll argument" do
    assert_no_errors <<-CRYSTAL
      @[Link(dll: "foo.dll")]
      lib LibFoo
      end
      CRYSTAL

    assert_no_errors <<-CRYSTAL
      @[Link(dll: "BAR.DLL")]
      lib LibFoo
      end
      CRYSTAL
  end

  it "errors if dll argument contains directory separators" do
    assert_error <<-CRYSTAL, "'dll' link argument must not include directory separators"
      @[Link(dll: "foo/bar.dll")]
      lib LibFoo
      end
      CRYSTAL

    assert_error <<-CRYSTAL, "'dll' link argument must not include directory separators"
      @[Link(dll: %q(foo\\bar.dll))]
      lib LibFoo
      end
      CRYSTAL
  end

  it "errors if dll argument does not end with '.dll'" do
    assert_error <<-CRYSTAL, "'dll' link argument must use a '.dll' file extension"
      @[Link(dll: "foo")]
      lib LibFoo
      end
      CRYSTAL

    assert_error <<-CRYSTAL, "'dll' link argument must use a '.dll' file extension"
      @[Link(dll: "foo.dylib")]
      lib LibFoo
      end
      CRYSTAL

    assert_error <<-CRYSTAL, "'dll' link argument must use a '.dll' file extension"
      @[Link(dll: "")]
      lib LibFoo
      end
      CRYSTAL
  end

  it "errors if lib already specified with positional argument" do
    assert_error %(
      @[Link("foo", lib: "bar")]
      lib LibFoo
      end
      ),
      "'lib' link argument already specified"
  end

  it "errors if lib named arg is not a String" do
    assert_error %(
      @[Link(lib: 1)]
      lib LibFoo
      end
      ),
      "'lib' link argument must be a String"
  end

  it "clears annotations after lib" do
    assert_type(%(
      @[Link("foo")]
      lib LibFoo
        fun foo
      end
      1
      )) { int32 }
  end

  it "warns if @[Link(static: true)] is specified" do
    assert_warning <<-CRYSTAL,
      @[Link("foo", static: true)]
      lib Foo
      end
      CRYSTAL
      "warning in line 1\nWarning: specifying static linking for individual libraries is deprecated"
  end

  it "warns if Link annotations use positional arguments" do
    assert_warning <<-CRYSTAL,
      @[Link("foo", "bar")]
      lib Foo
      end
      CRYSTAL
      "warning in line 1\nWarning: using non-named arguments for Link annotations is deprecated"
  end

  it "allows invoking lib call without obj inside lib" do
    assert_type(%(
      lib LibFoo
        fun foo : Int32

        A = foo
      end

      LibFoo::A
      )) { int32 }
  end

  it "errors if lib fun call is part of dispatch" do
    assert_error %(
      lib LibFoo
        fun foo : Int32
      end

      class Bar
        def self.foo
        end
      end

      (LibFoo || Bar).foo
      ),
      "lib fun call is not supported in dispatch"
  end

  it "disallows passing nil or pointer to arg expecting pointer" do
    assert_error %(
      lib Foo
        fun foo(x : Int32*) : Int64
      end

      a = 1 == 1 ? nil : Pointer(Int32).malloc(1_u64)
      Foo.foo(a)
      ),
      "argument 'x' of 'Foo#foo' must be Pointer(Int32), not (Pointer(Int32) | Nil)", inject_primitives: true
  end

  it "correctly attached link flags if there's a macro if" do
    result = semantic(%(
      @[Link("SDL")]
      @[Link("SDLMain")]
      {% if flag?(:some_flag) %}
        @[Link(framework: "Cocoa")]
      {% end %}
      lib LibSDL
        fun init = SDL_Init(flags : UInt32) : Int32
      end

      LibSDL.init(0_u32)
      ))
    sdl = result.program.types["LibSDL"].as(LibType)
    attrs = sdl.link_annotations.not_nil!
    attrs.size.should eq(2)
    attrs[0].lib.should eq("SDL")
    attrs[1].lib.should eq("SDLMain")
  end

  it "supports forward references (#399)" do
    assert_type(%(
      lib LibFoo
        fun foo() : Bar*

        struct Bar
          x : Int32
        end
      end

      LibFoo.foo
      )) { pointer_of(types["LibFoo"].types["Bar"]) }
  end

  it "supports forward references with struct inside struct (#399)" do
    assert_type(%(
      lib LibFoo
        struct Bar
          x : Foo*
        end

        struct Foo
          x : Int32
        end
      end

      LibFoo::Bar.new.x
      )) { pointer_of(types["LibFoo"].types["Foo"]) }
  end

  it "errors if defines def on lib" do
    assert_error %(
      lib LibC
      end

      def LibC.foo
      end
      ),
      "can't define method in lib LibC"
  end

  it "reopens lib and adds more link annotations" do
    result = semantic(%(
      @[Link("SDL")]
      lib LibSDL
        fun init = SDL_Init(flags : UInt32) : Int32
      end

      @[Link("SDLMain")]
      lib LibSDL
      end

      LibSDL.init(0_u32)
      ))
    sdl = result.program.types["LibSDL"].as(LibType)
    attrs = sdl.link_annotations.not_nil!
    attrs.size.should eq(2)
    attrs[0].lib.should eq("SDL")
    attrs[1].lib.should eq("SDLMain")
  end

  it "reopens lib and adds same link annotations" do
    result = semantic(%(
      @[Link("SDL")]
      lib LibSDL
        fun init = SDL_Init(flags : UInt32) : Int32
      end

      @[Link("SDL")]
      lib LibSDL
      end

      LibSDL.init(0_u32)
      ))
    sdl = result.program.types["LibSDL"].as(LibType)
    attrs = sdl.link_annotations.not_nil!
    attrs.size.should eq(1)
    attrs[0].lib.should eq("SDL")
  end

  it "gathers link annotations from macro expression" do
    result = semantic(%(
      {% begin %}
        @[Link("SDL")]
      {% end %}
      lib LibSDL
        fun init = SDL_Init : Int32
      end

      LibSDL.init
      ))
    sdl = result.program.types["LibSDL"].as(LibType)
    attrs = sdl.link_annotations.not_nil!
    attrs.size.should eq(1)
    attrs[0].lib.should eq("SDL")
  end

  it "errors if using void as parameter (related to #508)" do
    assert_error %(
      lib LibFoo
        fun foo(x : Void)
      end
      ),
      "can't use Void as parameter type"
  end

  it "errors if using void via typedef as parameter (related to #508)" do
    assert_error %(
      lib LibFoo
        type Foo = Void
        fun foo(x : Foo)
      end
      ),
      "can't use Void as parameter type"
  end

  it "can use tuple as fun return" do
    assert_type(%(
      lib LibC
        fun foo : {Int32, Int32}
      end

      LibC.foo
      )) { tuple_of([int32, int32] of TypeVar) }
  end

  it "doesn't try to invoke unsafe for c struct/union (#1362)" do
    assert_error %(
      lib LibFoo
        struct Bar
        end

        fun foo(x : Bar*)
      end

      bar = LibFoo::Bar.new
      LibFoo.foo(bar)
      ),
      "argument 'x' of 'LibFoo#foo' must be Pointer(LibFoo::Bar), not LibFoo::Bar"
  end

  it "passes int as another integer type in variable" do
    assert_type(%(
      lib LibFoo
        fun foo(x : Int32) : Float64
      end

      a = 1_u8
      LibFoo.foo a
      ), inject_primitives: true) { float64 }
  end

  it "passes float as another integer type in variable" do
    assert_type(%(
      lib LibFoo
        fun foo(x : Float32) : Int32
      end

      a = 1_f64
      LibFoo.foo a
      ), inject_primitives: true) { int32 }
  end

  it "passes int as another integer type with literal" do
    assert_type(%(
      lib LibFoo
        fun foo(x : Int32) : Float64
      end

      LibFoo.foo 1_u8
      )) { float64 }
  end

  it "errors if invoking to_i32! and got error in that call" do
    assert_error %(
      lib LibFoo
        fun foo(x : Int32) : Float64
      end

      class Foo
        def to_i32!
          1 + 'a'
        end
      end

      LibFoo.foo Foo.new
      ),
      "converting from Foo to Int32 by invoking 'to_i32!'"
  end

  it "errors if invoking to_i32! and got wrong type" do
    assert_error %(
      lib LibFoo
        fun foo(x : Int32) : Float64
      end

      class Foo
        def to_i32!
          'a'
        end
      end

      LibFoo.foo Foo.new
      ),
      "invoked 'to_i32!' to convert from Foo to Int32, but got Char"
  end

  it "defines lib funs before funs with body" do
    assert_type(%(
      fun foo : Int32
        LibX.x
      end

      lib LibX
        fun x : Int32
      end

      foo
      )) { int32 }
  end

  it "errors if using out with varargs" do
    assert_error %(
      lib LibX
        fun x(...)
      end

      LibX.x(out z)
      ),
      "can't use out at varargs position: declare the variable with `z = uninitialized ...` and pass it with `pointerof(z)`"
  end

  it "errors if using out with void pointer (#2424)" do
    assert_error %(
      lib LibFoo
        fun foo(x : Void*)
      end

      LibFoo.foo(out x)
      ),
      "can't use out with Void* (parameter 'x' of LibFoo.foo is Void*)"
  end

  it "errors if using out with void pointer through type" do
    assert_error %(
      lib LibFoo
        type Foo = Void
        fun foo(x : Foo*)
      end

      LibFoo.foo(out x)
      ),
      "can't use out with Void* (parameter 'x' of LibFoo.foo is Void*)"
  end

  it "errors if using out with non-pointer" do
    assert_error %(
      lib LibFoo
        fun foo(x : Int32)
      end

      LibFoo.foo(out x)
      ),
      "parameter 'x' of LibFoo.foo cannot be passed as 'out' because it is not a pointer"
  end

  it "errors if redefining fun with different signature (#2468)" do
    assert_error %(
      fun foo
      end

      fun foo(x : Int32)
      end
      ),
      "fun redefinition with different signature"
  end

  it "errors if using named args with variadic function" do
    assert_error %(
      lib LibC
        fun foo(x : Int32, y : UInt8, ...) : Int32
      end

      LibC.foo y: 1_u8, x: 1
      ),
      "can't use named args with variadic function"
  end

  it "errors if using unknown named param" do
    assert_error %(
      lib LibC
        fun foo(x : Int32, y : UInt8) : Int32
      end

      LibC.foo y: 1_u8, x: 1, z: 2
      ),
      "no parameter named 'z'"
  end

  it "errors if parameter already specified" do
    assert_error %(
      lib LibC
        fun foo(x : Int32, y : UInt8) : Int32
      end

      LibC.foo 1, x: 2
      ),
      "argument for parameter 'x' already specified"
  end

  it "errors if missing argument" do
    assert_error %(
      lib LibC
        fun foo(x : Int32, y : UInt8) : Int32
      end

      LibC.foo x: 2
      ),
      "missing argument: y"
  end

  it "errors if missing arguments" do
    assert_error %(
      lib LibC
        fun foo(x : Int32, y : UInt8, z: Int32) : Int32
      end

      LibC.foo y: 1_u8
      ),
      "missing arguments: x, z"
  end

  it "can use named args" do
    assert_type(%(
      lib LibC
        fun foo(x : Int32, y : UInt8) : Int32
      end

      LibC.foo y: 1_u8, x: 1
      )) { int32 }
  end

  it "can use out with named args" do
    assert_type(%(
      lib LibC
        fun foo(x : Int32*)
      end

      LibC.foo(x: out x)
      x
      )) { int32 }
  end

  it "types fun returning nothing as nil" do
    assert_type(%(
      lib LibFoo
        fun foo
      end

      LibFoo.foo
      )) { nil_type }
  end

  it "types fun returning void as nil" do
    assert_type(%(
      lib LibFoo
        fun foo : Void
      end

      LibFoo.foo
      )) { nil_type }
  end

  it "types fun returning nil as nil" do
    assert_type(%(
      lib LibFoo
        fun foo : Nil
      end

      LibFoo.foo
      )) { nil_type }
  end

  it "can use macros inside lib" do
    assert_type(%(
      lib LibFoo
        {% begin %}
          fun foo : Int32
        {% end %}
      end

      LibFoo.foo
      )) { int32 }
  end

  it "can use macros inside struct" do
    assert_type(%(
      lib LibFoo
        struct Foo
          {% begin %}
            x : Int32
          {% end %}
        end
      end

      LibFoo::Foo.new.x
      )) { int32 }
  end

  it "errors if defining incompatible funs with the same name in the same lib (#3045)" do
    assert_error %(
      lib LibFoo
        fun foo1 = foo
        fun foo2 = foo(x : Int32)
      end
      ),
      "fun redefinition with different signature"
  end

  it "errors if defining incompatible funs with the same name in different libs (#3045)" do
    assert_error %(
      lib LibFoo1
        fun foo1 = foo
      end

      lib LibFoo2
        fun foo2 = foo(x : Int32)
      end
      ),
      "fun redefinition with different signature"
  end

  it "specifies a call convention" do
    result = semantic(%(
      lib LibFoo
        @[CallConvention("X86_StdCall")]
        fun foo : Int32
      end
      ))
    foo = result.program.types["LibFoo"].lookup_first_def("foo", nil).as(External)
    foo.call_convention.should eq(LLVM::CallConvention::X86_StdCall)
  end

  it "specifies a call convention to a lib" do
    result = semantic(%(
      @[CallConvention("X86_StdCall")]
      lib LibFoo
        fun foo : Int32
      end
      ))
    foo = result.program.types["LibFoo"].lookup_first_def("foo", nil).as(External)
    foo.call_convention.should eq(LLVM::CallConvention::X86_StdCall)
  end

  it "errors if wrong number of arguments for CallConvention" do
    assert_error %(
      lib LibFoo
        @[CallConvention("X86_StdCall", "bar")]
        fun foo : Int32
      end
      ),
      "wrong number of arguments for annotation CallConvention (given 2, expected 1)"
  end

  it "errors if CallConvention argument is not a string" do
    assert_error %(
      lib LibFoo
        @[CallConvention(1)]
        fun foo : Int32
      end
      ),
      "argument to CallConvention must be a string"
  end

  it "errors if CallConvention argument is not a valid string" do
    assert_error %(
      lib LibFoo
        @[CallConvention("foo")]
        fun foo : Int32
      end
      ),
      "invalid call convention. Valid values are #{LLVM::CallConvention.values.join ", "}"
  end

  it "errors if assigning void lib call to var (#4414)" do
    assert_error %(
      lib LibFoo
        fun foo
      end

      x = LibFoo.foo
      ),
      "assigning Void return value of lib fun call has no effect"
  end

  it "errors if passing void lib call to call argument (#4414)" do
    assert_error %(
      lib LibFoo
        fun foo
      end

      def bar(x)
      end

      bar(LibFoo.foo)
      ),
      "passing Void return value of lib fun call has no effect"
  end

  it "can list lib functions at the top level (#12395)" do
    assert_type(%(
      lib LibFoo
        fun foo
      end

      {% if LibFoo.methods.size == 1 %}
        true
      {% else %}
        1
      {% end %}
      )) { bool }
  end
end
