module private FsAutoComplete.Tests.CodeFixTests.AddExplicitTypeAnnotationTests

open Expecto
open Helpers
open Utils.ServerTests
open Utils.CursorbasedTests
open FsAutoComplete.CodeFix

let tests state =
  serverTestList (nameof AddExplicitTypeAnnotation) state defaultConfigDto None (fun server -> [
    let selectCodeFix = CodeFix.withTitle AddExplicitTypeAnnotation.title
    testCaseAsync "can suggest explicit parameter for record-typed function parameters" <|
      CodeFix.check server
        """
        type Foo =
            { name: string }

        let name $0f =
            f.name
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        type Foo =
            { name: string }

        let name (f: Foo) =
            f.name
        """
    testCaseAsync "can add type for int param" <|
      CodeFix.check server
        """
        let f ($0x) = x + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        let f (x: int) = x + 1
        """
    testCaseAsync "can add type for generic param" <|
      CodeFix.check server
        """
        let f ($0x) = ()
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        let f (x: 'a) = ()
        """
    testCaseAsync "doesn't trigger when existing type" <|
      CodeFix.checkNotApplicable server
        """
        let f ($0x: int) = ()
        """
        (Diagnostics.acceptAll)
        selectCodeFix
    testCaseAsync "can add type to tuple item" <|
      CodeFix.check server
        """
        let f (a, $0b, c) = a + b + c + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        let f (a, b: int, c) = a + b + c + 1
        """
    testCaseAsync "doesn't trigger in tuple when existing type" <|
      CodeFix.checkNotApplicable server
        """
        let f (a, $0b: int, c) = a + b + c + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
    testCaseAsync "can add type to 2nd of 3 param" <|
      CodeFix.check server
        """
        let f a $0b c = a + b + c + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        let f a (b: int) c = a + b + c + 1
        """
    testCaseAsync "doesn't trigger on 2nd of 3 param when existing type" <|
      CodeFix.checkNotApplicable server
        """
        let f a ($0b: int) c = a + b + c + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
    testCaseAsync "can add type to 2nd of 3 param when other params have types" <|
      CodeFix.check server
        """
        let f (a: int) $0b (c: int) = a + b + c + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        let f (a: int) (b: int) (c: int) = a + b + c + 1
        """
    testCaseAsync "can add type to member param" <|
      CodeFix.check server
        """
        type A() =
          member _.F($0a) = a + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        type A() =
          member _.F(a: int) = a + 1
        """
    testCaseAsync "doesn't trigger for member param when existing type" <|
      CodeFix.checkNotApplicable server
        """
        type A() =
          member _.F($0a: int) = a + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
    testCaseAsync "can add type to ctor param" <|
      CodeFix.check server
        """
        type A($0a) =
          member _.F() = a + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        type A(a: int) =
          member _.F() = a + 1
        """
    testCaseAsync "doesn't trigger for ctor param when existing type" <|
      CodeFix.checkNotApplicable server
        """
        type A($0a: int) =
          member _.F() = a + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
    testCaseAsync "can add type to correct ctor param" <|
      CodeFix.check server
        """
        type A(str, $0n, b) =
          member _.FString() = sprintf "str=%s" str
          member _.FInt() = n + 1
          member _.FBool() = sprintf "b=%b" b
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        type A(str, n: int, b) =
          member _.FString() = sprintf "str=%s" str
          member _.FInt() = n + 1
          member _.FBool() = sprintf "b=%b" b
        """
    testCaseAsync "doesn't trigger for ctor param when existing type and multiple params" <|
      CodeFix.checkNotApplicable server
        """
        type A(str, $0n: int, b) =
          member _.FString() = sprintf "str=%s" str
          member _.FInt() = a + 1
          member _.FBool() = sprintf "b=%b" b
        """
        (Diagnostics.acceptAll)
        selectCodeFix
    testCaseAsync "can add type to secondary ctor param" <|
      CodeFix.check server
        """
        type A(a) =
          new($0a, b) = A(a+b)
          member _.F() = a + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        type A(a) =
          new(a: int, b) = A(a+b)
          member _.F() = a + 1
        """
    testCaseAsync "can add type to function parameter" <|
      CodeFix.check server
        """
        let map $0f v = f (v+1) + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        let map (f: int -> int) v = f (v+1) + 1
        """
    testCaseAsync "can add type to function member parameter" <|
      CodeFix.check server
        """
        type A(a) =
          member _.F(b, $0f) = f (a+b) + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        type A(a) =
          member _.F(b, f: int -> int) = f (a+b) + 1
        """
    testCaseAsync "can add type to function value" <|
      CodeFix.check server
        """
        let $0f = fun a b -> a + b
        """
        (Diagnostics.acceptAll)
        selectCodeFix
        """
        let f: int -> int -> int = fun a b -> a + b
        """
    testCaseAsync "doesn't trigger for function value with existing type annotation" <|
      CodeFix.checkNotApplicable server
        """
        let $0f: int -> int -> int = fun a b -> a + b
        """
        (Diagnostics.acceptAll)
        selectCodeFix
    testCaseAsync "doesn't trigger for function parameter with existing type annotation" <|
      CodeFix.checkNotApplicable server
        """
        let map ($0f: int -> int) v = f (v+1) + 1
        """
        (Diagnostics.acceptAll)
        selectCodeFix

    testList "parens" [
      testCaseAsync "single param without parens -> add parens" <|
        CodeFix.check server
          """
          let f $0x = x + 1
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          let f (x: int) = x + 1
          """
      testCaseAsync "single param with parens -> keep parens" <|
        CodeFix.check server
          """
          let f ($0x) = x + 1
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          let f (x: int) = x + 1
          """
      testCaseAsync "multi params without parens -> add parens" <|
        CodeFix.check server
          """
          let f a $0x y = x + 1
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          let f a (x: int) y = x + 1
          """
      testCaseAsync "multi params with parens -> keep parens" <|
        CodeFix.check server
          """
          let f a ($0x) y = x + 1
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          let f a (x: int) y = x + 1
          """
      testList "tuple params without parens -> no parens" [
        testCaseAsync "start" <|
          CodeFix.check server
            """
            let f ($0x, y, z) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (x: int, y, z) = x + y + z + 1
            """
        testCaseAsync "center" <|
          CodeFix.check server
            """
            let f (x, $0y, z) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (x, y: int, z) = x + y + z + 1
            """
        testCaseAsync "end" <|
          CodeFix.check server
            """
            let f (x, y, $0z) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (x, y, z: int) = x + y + z + 1
            """
      ]
      testList "tuple params with parens -> keep parens" [
        testCaseAsync "start" <|
          CodeFix.check server
            """
            let f (($0x), y, z) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f ((x: int), y, z) = x + y + z + 1
            """
        testCaseAsync "center" <|
          CodeFix.check server
            """
            let f (x, ($0y), z) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (x, (y: int), z) = x + y + z + 1
            """
        testCaseAsync "end" <|
          CodeFix.check server
            """
            let f (x, y, ($0z)) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (x, y, (z: int)) = x + y + z + 1
            """
      ]
      testList "tuple params without parens but spaces -> no parens" [
        testCaseAsync "start" <|
          CodeFix.check server
            """
            let f (  $0x   ,   y   ,   z   ) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (  x: int   ,   y   ,   z   ) = x + y + z + 1
            """
        testCaseAsync "center" <|
          CodeFix.check server
            """
            let f (  x   ,   $0y   ,   z   ) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (  x   ,   y: int   ,   z   ) = x + y + z + 1
            """
        testCaseAsync "end" <|
          CodeFix.check server
            """
            let f (  x   ,   y   ,   $0z   ) = x + y + z + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (  x   ,   y   ,   z: int   ) = x + y + z + 1
            """
      ]
      testList "long tuple params without parens but spaces -> no parens" [
        testCaseAsync "start" <|
          CodeFix.check server
            """
            let f (  xV$0alue   ,   yAnotherValue   ,   zFinalValue   ) = xValue + yAnotherValue + zFinalValue + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (  xValue: int   ,   yAnotherValue   ,   zFinalValue   ) = xValue + yAnotherValue + zFinalValue + 1
            """
        testCaseAsync "center" <|
          CodeFix.check server
            """
            let f (  xValue   ,   yAn$0otherValue   ,   zFinalValue   ) = xValue + yAnotherValue + zFinalValue + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (  xValue   ,   yAnotherValue: int   ,   zFinalValue   ) = xValue + yAnotherValue + zFinalValue + 1
            """
        testCaseAsync "end" <|
          CodeFix.check server
            """
            let f (  xValue   ,   yAnotherValue   ,   zFina$0lValue   ) = xValue + yAnotherValue + zFinalValue + 1
            """
            (Diagnostics.acceptAll)
            selectCodeFix
            """
            let f (  xValue   ,   yAnotherValue   ,   zFinalValue: int   ) = xValue + yAnotherValue + zFinalValue + 1
            """
      ]
      testCaseAsync "never add parens to primary ctor param" <|
        CodeFix.check server
          """
          type A (
            $0a
            ) =
            member _.F(b) = a + b
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          type A (
            a: int
            ) =
            member _.F(b) = a + b
          """

      testCaseAsync "emit type for optional parameter without option" <|
        CodeFix.check server
          """
          type A =
            static member F(?$0a) = a |> Option.map ((+) 1)
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          type A =
            static member F(?a: int) = a |> Option.map ((+) 1)
          """
      testCaseAsync "adds parens to optional parameter" <|
        CodeFix.check server
          """
          type A =
            static member F?$0a = a |> Option.map ((+) 1)
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          type A =
            static member F(?a: int) = a |> Option.map ((+) 1)
          """
      testCaseAsync "adds parens to ident in match case" <|
        CodeFix.check server
          """
          match 4 with
          | $0value -> ()
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          match 4 with
          | (value: int) -> ()
          """

      testCaseAsync "doesn't add parens to let" <|
        CodeFix.check server
          """
          let $0value = 42
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          let value: int = 42
          """
      testCaseAsync "adds parens to let!" <|
        CodeFix.check server
          """
          async {
            let! $0value = async { return 4 }
            ()
          } |> ignore
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          async {
            let! (value: int) = async { return 4 }
            ()
          } |> ignore
          """
      testCaseAsync "doesn't add parens to use" <|
        CodeFix.check server
          """
          open System
          let d = { new IDisposable with
              member _.Dispose () = ()
          }
          let _ =
              use $0value = d
              ()
          """
          (Diagnostics.acceptAll)
          selectCodeFix
          """
          open System
          let d = { new IDisposable with
              member _.Dispose () = ()
          }
          let _ =
              use value: IDisposable = d
              ()
          """
      testCaseAsync "doesn't trigger for use!" <|
        CodeFix.checkNotApplicable server
          """
          open System
          let d = { new IDisposable with
              member _.Dispose () = ()
          }
          async {
              use! $0value = async { return d }
              ()
          } |> ignore
          """
          (Diagnostics.acceptAll)
          selectCodeFix

      testCaseAsync "add type annotations to entire function" <|
        CodeFix.check server
          """
          let x$0 y z =
              ignore<int> y
              ignore<char> z
              0
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          let x (y: int) (z: char) : int =
              ignore<int> y
              ignore<char> z
              0
          """

      testCaseAsync "add type annotations to function with lambda function body" <|
        CodeFix.check server
          """
          let f1$0 a = fun b -> a + b
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          let f1 (a: int) : int -> int = fun b -> a + b
          """

      testCaseAsync "add type annotations to function with sequential lambda function body" <|
        CodeFix.check server
          """
          let f2$0 a =
              ()
              fun b -> a + b
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          let f2 (a: int) : int -> int =
              ()
              fun b -> a + b
          """

      testCaseAsync "add return type annotation when other parameters are typed" <|
        CodeFix.check server
          """
          let f1$0 (a:int) = fun (b:char) -> System.TimeSpan.Zero
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          let f1 (a:int) : char -> System.TimeSpan = fun (b:char) -> System.TimeSpan.Zero
          """

      testCaseAsync "add return type annotation for match lambda" <|
        CodeFix.check server
          """
          let f1$0 = function | None -> 1 | Some _ -> 2
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          let f1: 'a option -> int = function | None -> 1 | Some _ -> 2
          """

      testCaseAsync "add return type annotation when cursor is on let keyword" <|
        CodeFix.check server
          """
          let$0 f g h = ignore<int> g ; ignore<string> h ; - 9.0
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          let f (g: int) (h: string) : float = ignore<int> g ; ignore<string> h ; - 9.0
          """

      testCaseAsync "add type annotation for parameter when cursor is on function name" <|
        CodeFix.check server
          """
          let f$0 (a:int) b : int = a + b
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          let f (a:int) (b: int) : int = a + b
          """

      testCaseAsync "add type annotation for local function" <|
        CodeFix.check server
          """
          do
              let f$0 a b = a + b
              ()
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          do
              let f (a: int) (b: int) : int = a + b
              ()
          """

      testCaseAsync "add type annotation for recursive function" <|
        CodeFix.check server
          """
          let rec a b = b - 1
          and c$0 d e = d - e
          """
          Diagnostics.acceptAll
          selectCodeFix
          """
          let rec a b = b - 1
          and c (d: int) (e: int) : int = d - e
          """
    ]
  ])
