//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

@_spi(SourceKitLSP) import LanguageServerProtocol
import SKLogging
import SKTestSupport
import SourceKitLSP
import SwiftExtensions
@_spi(Testing) import SwiftLanguageService
import SwiftParser
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxCodeActions
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
import XCTest

private typealias CodeActionCapabilities = TextDocumentClientCapabilities.CodeAction
private typealias CodeActionLiteralSupport = CodeActionCapabilities.CodeActionLiteralSupport
private typealias CodeActionKindCapabilities = CodeActionLiteralSupport.CodeActionKindValueSet

private let clientCapabilitiesWithCodeActionSupport: ClientCapabilities = {
  var documentCapabilities = TextDocumentClientCapabilities()
  var codeActionCapabilities = CodeActionCapabilities()
  let codeActionKinds = CodeActionKindCapabilities(valueSet: [.refactor, .quickFix])
  let codeActionLiteralSupport = CodeActionLiteralSupport(codeActionKind: codeActionKinds)
  codeActionCapabilities.codeActionLiteralSupport = codeActionLiteralSupport
  documentCapabilities.codeAction = codeActionCapabilities
  documentCapabilities.completion = .init(completionItem: .init(snippetSupport: true))
  return ClientCapabilities(workspace: nil, textDocument: documentCapabilities)
}()

final class CodeActionTests: SourceKitLSPTestCase {
  func testCodeActionResponseLegacySupport() throws {
    let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil])
    let codeAction = CodeAction(title: "1")
    let codeAction2 = CodeAction(title: "2", command: command)

    var capabilities: TextDocumentClientCapabilities.CodeAction
    var capabilityJson: String
    var data: Data
    var response: CodeActionRequestResponse
    capabilityJson =
      """
       {
         "dynamicRegistration": true,
         "codeActionLiteralSupport" : {
           "codeActionKind": {
             "valueSet": []
           }
         }
       }
      """
    data = capabilityJson.data(using: .utf8)!
    capabilities = try JSONDecoder().decode(
      TextDocumentClientCapabilities.CodeAction.self,
      from: data
    )
    response = .init(codeActions: [codeAction, codeAction2], clientCapabilities: capabilities)
    let actions = try JSONDecoder().decode([CodeAction].self, from: JSONEncoder().encode(response))
    XCTAssertEqual(actions, [codeAction, codeAction2])

    capabilityJson =
      """
      {
        "dynamicRegistration": true
      }
      """
    data = capabilityJson.data(using: .utf8)!
    capabilities = try JSONDecoder().decode(
      TextDocumentClientCapabilities.CodeAction.self,
      from: data
    )
    response = .init(codeActions: [codeAction, codeAction2], clientCapabilities: capabilities)
    let commands = try JSONDecoder().decode([Command].self, from: JSONEncoder().encode(response))
    XCTAssertEqual(commands, [command])
  }

  func testCodeActionResponseIgnoresSupportedKinds() throws {
    // The client guarantees that unsupported kinds will be handled, and in
    // practice some clients use `"codeActionKind":{"valueSet":[]}`, since
    // they support all kinds anyway. So to avoid filtering all actions, we
    // ignore the supported kinds.

    let unspecifiedAction = CodeAction(title: "Unspecified")
    let refactorAction = CodeAction(title: "Refactor", kind: .refactor)
    let quickfixAction = CodeAction(title: "Quickfix", kind: .quickFix)
    let actions = [unspecifiedAction, refactorAction, quickfixAction]

    var capabilities: TextDocumentClientCapabilities.CodeAction
    var capabilityJson: String
    var data: Data
    var response: CodeActionRequestResponse
    capabilityJson =
      """
      {
        "dynamicRegistration": true,
        "codeActionLiteralSupport" : {
          "codeActionKind": {
            "valueSet": ["refactor"]
          }
        }
      }
      """
    data = capabilityJson.data(using: .utf8)!
    capabilities = try JSONDecoder().decode(
      TextDocumentClientCapabilities.CodeAction.self,
      from: data
    )

    response = .init(codeActions: actions, clientCapabilities: capabilities)
    XCTAssertEqual(response, .codeActions([unspecifiedAction, refactorAction, quickfixAction]))

    capabilityJson =
      """
      {
        "dynamicRegistration": true,
        "codeActionLiteralSupport" : {
          "codeActionKind": {
            "valueSet": []
          }
        }
      }
      """
    data = capabilityJson.data(using: .utf8)!
    capabilities = try JSONDecoder().decode(
      TextDocumentClientCapabilities.CodeAction.self,
      from: data
    )

    response = .init(codeActions: actions, clientCapabilities: capabilities)
    XCTAssertEqual(response, .codeActions([unspecifiedAction, refactorAction, quickfixAction]))
  }

  func testCodeActionResponseCommandMetadataInjection() throws {
    let url = URL(fileURLWithPath: "/a.swift")
    let textDocument = TextDocumentIdentifier(url)
    let expectedCommandMetadata: LSPAny = try {
      let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument)
      let data = try JSONEncoder().encode(metadata)
      return try JSONDecoder().decode(LSPAny.self, from: data)
    }()
    XCTAssertEqual(expectedCommandMetadata, ["sourcekitlsp_textDocument": ["uri": "file:///a.swift"]])

    let expectedResolveMetadata: LSPAny = try {
      let metadata = CodeActionResolveMetadata(textDocument: textDocument)
      let data = try JSONEncoder().encode(metadata)
      return try JSONDecoder().decode(LSPAny.self, from: data)
    }()
    XCTAssertEqual(expectedResolveMetadata, ["textDocument": ["uri": "file:///a.swift"]])

    let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil])
    let codeAction = CodeAction(title: "1")
    let codeAction2 = CodeAction(title: "2", command: command)
    let request = CodeActionRequest(
      range: Position(line: 0, utf16index: 0)..<Position(line: 1, utf16index: 1),
      context: .init(diagnostics: [], only: nil),
      textDocument: textDocument
    )
    var response = request.injectMetadata(toResponse: .commands([command]))
    XCTAssertEqual(
      response,
      .commands([
        Command(
          title: command.title,
          command: command.command,
          arguments: command.arguments! + [expectedCommandMetadata]
        )
      ])
    )
    response = request.injectMetadata(toResponse: .codeActions([codeAction, codeAction2]))
    var expectedCodeAction = codeAction
    expectedCodeAction.data = expectedResolveMetadata
    XCTAssertEqual(
      response,
      .codeActions([
        expectedCodeAction,
        CodeAction(
          title: codeAction2.title,
          command: Command(
            title: command.title,
            command: command.command,
            arguments: command.arguments! + [expectedCommandMetadata]
          )
        ),
      ])
    )
    response = request.injectMetadata(toResponse: nil)
    XCTAssertNil(response)
  }

  func testCodeActionResolveMetadataPreservesUnderlyingData() throws {
    let url = URL(fileURLWithPath: "/a.swift")
    let textDocument = TextDocumentIdentifier(url)

    let originalData: LSPAny = ["custom": "value"]
    var codeAction = CodeAction(title: "With data")
    codeAction.data = originalData

    let request = CodeActionRequest(
      range: Position(line: 0, utf16index: 0)..<Position(line: 1, utf16index: 1),
      context: .init(diagnostics: [], only: nil),
      textDocument: textDocument
    )
    let response = request.injectMetadata(toResponse: .codeActions([codeAction]))

    guard case .codeActions(let actions) = response, let injected = actions.first,
      case .dictionary(let dict) = injected.data,
      let metadata = CodeActionResolveMetadata(fromLSPDictionary: dict)
    else {
      return XCTFail("Expected resolve metadata in data")
    }

    XCTAssertEqual(metadata.textDocument, textDocument)
    XCTAssertEqual(metadata.underlyingData, originalData)
  }

  func testCommandEncoding() throws {
    let dictionary: LSPAny = ["1": [nil, 2], "2": "text", "3": ["4": [1, 2]]]
    let array: LSPAny = [1, [2, "string"], dictionary]
    let arguments: LSPAny = [1, 2.2, "text", nil, array, dictionary]
    let command = Command(title: "Command", command: "command.id", arguments: [arguments, arguments])
    let decoded = try JSONDecoder().decode(Command.self, from: JSONEncoder().encode(command))
    XCTAssertEqual(decoded, command)
  }

  func testEmptyCodeActionResult() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      func foo() -> String {
        var a = "hello"
      1️⃣  return a
      }

      """,
      uri: uri
    )

    let request = CodeActionRequest(
      range: positions["1️⃣"]..<positions["1️⃣"],
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)
    XCTAssertEqual(result, .codeActions([]))
  }

  func testSemanticRefactorLocalRenameResult() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      func localRename() {
        var 1️⃣local = 1
        _ = local
      }
      """,
      uri: uri
    )

    let request = CodeActionRequest(
      range: Range(positions["1️⃣"]),
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)
    XCTAssertEqual(
      result?.codeActions?.map(\.title),
      ["Add documentation", "Convert Stored Property to Computed Property"]
    )
  }

  func testSemanticRefactorLocationCodeActionResult() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      func foo() -> String {
        var a = "1️⃣"
        return a
      }
      """,
      uri: uri
    )

    let testPosition = positions["1️⃣"]
    let request = CodeActionRequest(
      range: Range(testPosition),
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)

    let expectedCommandArgs: LSPAny = [
      "actionString": "source.refactoring.kind.localize.string",
      "positionRange": [
        "start": [
          "character": .int(testPosition.utf16index),
          "line": .int(testPosition.line),
        ],
        "end": [
          "character": .int(testPosition.utf16index),
          "line": .int(testPosition.line),
        ],
      ],
      "title": "Localize String",
      "textDocument": ["uri": .string(uri.stringValue)],
    ]

    let metadataArguments: LSPAny = ["sourcekitlsp_textDocument": ["uri": .string(uri.stringValue)]]
    let expectedCommand = Command(
      title: "Localize String",
      command: "semantic.refactor.command",
      arguments: [expectedCommandArgs] + [metadataArguments]
    )
    let expectedCodeAction = CodeAction(
      title: "Localize String",
      kind: .refactor,
      command: expectedCommand
    )

    assertContains(result?.codeActions ?? [], expectedCodeAction)
  }

  func testJSONCodableCodeActionResult() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
         1️⃣{
         "name": "Produce",
         "shelves": [
             {
                 "name": "Discount Produce",
                 "product": {
                     "name": "Banana",
                     "points": 200,
                     "description": "A banana that's perfectly ripe."
                 }
             }
         ]
      }
      """,
      uri: uri
    )

    let testPosition = positions["1️⃣"]
    let request = CodeActionRequest(
      range: Range(testPosition),
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)

    // Make sure we get a JSON conversion action.
    let codableAction = result?.codeActions?.first { action in
      return action.title == "Create Codable structs from JSON"
    }
    XCTAssertNotNil(codableAction)
  }

  func testSemanticRefactorRangeCodeActionResult() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      func foo() -> String {
        1️⃣var a = "hello"
        return a2️⃣
      }
      """,
      uri: uri
    )

    let startPosition = positions["1️⃣"]
    let endPosition = positions["2️⃣"]
    let request = CodeActionRequest(
      range: startPosition..<endPosition,
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)

    let expectedCommandArgs: LSPAny = [
      "actionString": "source.refactoring.kind.extract.function",
      "positionRange": [
        "start": [
          "character": .int(startPosition.utf16index),
          "line": .int(startPosition.line),
        ],
        "end": [
          "character": .int(endPosition.utf16index),
          "line": .int(endPosition.line),
        ],
      ],
      "title": "Extract Method",
      "textDocument": ["uri": .string(uri.stringValue)],
    ]
    let metadataArguments: LSPAny = ["sourcekitlsp_textDocument": ["uri": .string(uri.stringValue)]]
    let expectedCommand = Command(
      title: "Extract Method",
      command: "semantic.refactor.command",
      arguments: [expectedCommandArgs] + [metadataArguments]
    )
    let expectedCodeAction = CodeAction(
      title: "Extract Method",
      kind: .refactorExtract,
      command: expectedCommand
    )
    var resultActions = try XCTUnwrap(result?.codeActions)

    // Filter out "Add documentation"; we test it elsewhere
    if let addDocIndex = resultActions.firstIndex(where: {
      $0.title == "Add documentation"
    }
    ) {
      resultActions.remove(at: addDocIndex)
    } else {
      XCTFail("Missing 'Add documentation'.")
      return
    }

    XCTAssertEqual(resultActions, [expectedCodeAction])
  }

  func testCodeActionsRemovePlaceholders() async throws {
    let testClient = try await TestSourceKitLSPClient(
      capabilities: clientCapabilitiesWithCodeActionSupport,
      usePullDiagnostics: false
    )
    let uri = DocumentURI(for: .swift)

    let positions = testClient.openDocument(
      """
      protocol MyProto {
        func foo()
      }

      struct 1️⃣MyStruct: MyProto {

      }
      """,
      uri: uri
    )

    let diags = try await testClient.nextDiagnosticsNotification()
    XCTAssertEqual(diags.uri, uri)
    XCTAssertEqual(diags.diagnostics.count, 1)
    let diagPosition = try XCTUnwrap(diags.diagnostics.only?.range.lowerBound)

    let quickFixActionResult = try await testClient.send(
      CodeActionRequest(
        range: Range(diagPosition),
        context: .init(diagnostics: diags.diagnostics),
        textDocument: TextDocumentIdentifier(uri)
      )
    )

    // Check that the Fix-It action contains snippets

    guard let quickFixAction = quickFixActionResult?.codeActions?.filter({ $0.kind == .quickFix }).only else {
      return XCTFail("Expected exactly one quick fix action")
    }
    guard let change = quickFixAction.edit?.changes?[uri]?.only else {
      return XCTFail("Expected exactly one change")
    }
    XCTAssertEqual(
      change.newText.trimmingTrailingWhitespace(),
      """

          func foo() {

          }

      """
    )

    // Check that the refactor action contains snippets
    let refactorActionResult = try await testClient.send(
      CodeActionRequest(
        range: Range(positions["1️⃣"]),
        context: .init(diagnostics: diags.diagnostics),
        textDocument: TextDocumentIdentifier(uri)
      )
    )

    guard let refactorAction = refactorActionResult?.codeActions?.filter({ $0.kind == .refactor }).only else {
      return XCTFail("Expected exactly one refactor action")
    }
    guard let command = refactorAction.command else {
      return XCTFail("Expected the refactor action to have a command")
    }

    let editReceived = self.expectation(description: "Received ApplyEdit request")

    testClient.handleSingleRequest { (request: ApplyEditRequest) -> ApplyEditResponse in
      defer {
        editReceived.fulfill()
      }
      guard let change = request.edit.changes?[uri]?.only else {
        XCTFail("Expected exactly one edit")
        return ApplyEditResponse(applied: false, failureReason: "Expected exactly one edit")
      }
      XCTAssertEqual(
        change.newText.trimmingTrailingWhitespace(),
        """

            func foo() {

            }

        """
      )
      return ApplyEditResponse(applied: true, failureReason: nil)
    }
    _ = try await testClient.send(ExecuteCommandRequest(command: command.command, arguments: command.arguments))

    try await fulfillmentOfOrThrow(editReceived)
  }

  func testAddDocumentationCodeActionResult() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      2️⃣func refacto1️⃣r(syntax: DeclSyntax, in context: Void) -> DeclSyntax? { }3️⃣
      """,
      uri: uri
    )

    let testPosition = positions["1️⃣"]
    let request = CodeActionRequest(
      range: Range(testPosition),
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)

    // Make sure we get an add-documentation action.
    let addDocAction = result?.codeActions?.first { action in
      return action.title == "Add documentation"
    }
    XCTAssertNotNil(addDocAction)
  }

  func testCodeActionForFixItsProducedBySwiftSyntax() async throws {
    let project = try await MultiFileTestProject(files: [
      "test.swift": "protocol 1️⃣Multi2️⃣ 3️⃣ident 4️⃣{}",
      "compile_commands.json": "[]",
    ])

    let (uri, positions) = try project.openDocument("test.swift")

    let report = try await project.testClient.send(
      DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
    )

    XCTAssertEqual(report.fullReport?.items.count, 1)
    let codeActions = try XCTUnwrap(report.fullReport?.items.first?.codeActions)

    let expectedCodeActions = [
      CodeAction(
        title: "Join the identifiers together",
        kind: .quickFix,
        edit: WorkspaceEdit(
          changes: [
            uri: [
              TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "Multiident"),
              TextEdit(range: positions["3️⃣"]..<positions["4️⃣"], newText: ""),
            ]
          ]
        )
      ),
      CodeAction(
        title: "Join the identifiers together with camel-case",
        kind: .quickFix,
        edit: WorkspaceEdit(
          changes: [
            uri: [
              TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "MultiIdent"),
              TextEdit(range: positions["3️⃣"]..<positions["4️⃣"], newText: ""),
            ]
          ]
        )
      ),
    ]
    XCTAssertEqual(expectedCodeActions, codeActions)
  }

  func testPackageManifestEditingCodeActionResult() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      // swift-tools-version: 5.5
      let package = Package(
          name: "packages",
          targets: [
              .tar1️⃣get(name: "MyLib"),
          ]
      )
      """,
      uri: uri
    )

    let testPosition = positions["1️⃣"]
    let request = CodeActionRequest(
      range: Range(testPosition),
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)

    let codeActions = try XCTUnwrap(result?.codeActions)

    // Make sure we get the expected package manifest editing actions.
    let addTestAction = codeActions.first { action in
      return action.title == "Add test target (Swift Testing)"
    }
    XCTAssertNotNil(addTestAction)

    XCTAssertTrue(
      codeActions.contains { action in
        action.title == "Add library target"
      }
    )

    guard let addTestChanges = addTestAction?.edit?.changes else {
      XCTFail("Didn't have changes in the 'Add test target (Swift Testing)' action")
      return
    }

    guard let manifestEdits = addTestChanges[uri] else {
      XCTFail("Didn't have edits")
      return
    }

    XCTAssertTrue(
      manifestEdits.contains { edit in
        edit.newText.contains("testTarget")
      }
    )

    XCTAssertTrue(
      codeActions.contains { action in
        return action.title == "Add product to export this target"
      }
    )
  }

  func testPackageManifestEditingCodeActionNoTestResult() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      // swift-tools-version: 5.5
      let package = Package(
          name: "packages",
          targets: [
              .testTar1️⃣get(name: "MyLib"),
          ]
      )
      """,
      uri: uri
    )

    let testPosition = positions["1️⃣"]
    let request = CodeActionRequest(
      range: Range(testPosition),
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)

    let codeActions = try XCTUnwrap(result?.codeActions)

    // Make sure we get the expected package manifest editing actions.
    XCTAssertTrue(
      !codeActions.contains { action in
        return action.title == "Add test target"
      }
    )

    XCTAssertTrue(
      !codeActions.contains { action in
        return action.title == "Add product to export this target"
      }
    )
  }

  func testConvertIntegerLiteral() async throws {
    try await assertCodeActions(
      """
      let x = 1️⃣12️⃣63️⃣
      """,
      ranges: [("1️⃣", "2️⃣"), ("1️⃣", "3️⃣")],
      ignoringCodeActions: ["Convert to computed property"]
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert 16 to 0b10000",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "0b10000")]]
          )
        ),
        CodeAction(
          title: "Convert 16 to 0o20",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "0o20")]]
          )
        ),
        CodeAction(
          title: "Convert 16 to 0x10",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "0x10")]]
          )
        ),
      ]
    }
  }

  func testAddSeparatorsToIntegerLiteral() async throws {
    try await assertCodeActions(
      """
      let x = 1️⃣10002️⃣
      """,
      markers: ["1️⃣"],
      ignoringCodeActions: ["Convert to computed property"]
    ) { uri, positions in
      [
        CodeAction(
          title: "Add digit separators",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "1_000")]]
          )
        ),
        CodeAction(
          title: "Convert 1000 to 0b1111101000",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "0b1111101000")]]
          )
        ),
        CodeAction(
          title: "Convert 1000 to 0o1750",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "0o1750")]]
          )
        ),
        CodeAction(
          title: "Convert 1000 to 0x3e8",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "0x3e8")]]
          )
        ),
      ]
    }
  }

  func testAddExplicitEnumRawValuesInt() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Status: Int {
        case active2️⃣
        case inactive3️⃣
        case pending = 10
        case archived4️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Add Explicit Raw Values",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = 0"),
                TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = 1"),
                TextEdit(range: positions["4️⃣"]..<positions["4️⃣"], newText: " = 11"),
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddExplicitEnumRawValuesString() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Direction: String {
        case north2️⃣
        case south = "down"
        case east3️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Add Explicit Raw Values",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = \"north\""),
                TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = \"east\""),
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddExplicitEnumRawValuesContinuesAfterNegativeLiteral() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Offset: Int {
        case low = -5
        case medium2️⃣
        case high3️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Add Explicit Raw Values",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = -4"),
                TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = -3"),
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddExplicitEnumRawValuesContinuesAfterHexLiteral() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Mask: Int {
        case bit0 = 0x10
        case bit12️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Add Explicit Raw Values",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = 17")
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddExplicitEnumRawValuesNotOfferedWhenAllCasesExplicit() async throws {
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        1️⃣enum Status: Int {
          case active = 0
          case inactive = 1
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testAddExplicitEnumRawValuesNotOfferedWithoutRawValueType() async throws {
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        1️⃣enum Direction {
          case north
          case south
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testAddExplicitEnumRawValuesNotOfferedWithUnsupportedRawValue() async throws {
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        let base = 10
        1️⃣enum Tier: Int {
          case low = base
          case high
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testAddExplicitEnumRawValuesStripsBackticksInImplicitStringRawValue() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Keyword: String {
        case `default`2️⃣
        case ifBranch = "if"
        case `class`3️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Add Explicit Raw Values",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = \"default\""),
                TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = \"class\""),
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddExplicitEnumRawValuesNotOfferedWhenRawValueTypeIsNotFirst() async throws {
    // Per Swift's grammar, only the first inherited type may specify a raw
    // value. `: CaseIterable, Int` is invalid Swift, so the code action must
    // not be offered.
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        1️⃣enum Status: CaseIterable, Int {
          case active
          case inactive
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testAddExplicitEnumRawValuesNotOfferedWithIfConfigMembers() async throws {
    // `#if` blocks change which cases are active in a given configuration, so
    // the implicit raw value counter cannot be computed purely syntactically.
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        1️⃣enum Mode: Int {
          case a
          #if DEBUG
          case b
          #endif
          case c
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testAddExplicitEnumRawValuesNotOfferedWithFreestandingMacro() async throws {
    // Freestanding declaration macros can expand to enum cases, so the counter
    // cannot be computed without expanding the macro.
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        1️⃣enum Mode: Int {
          case a
          #generatedCases()
          case b
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testAddExplicitEnumRawValuesNotOfferedWithAssociatedValues() async throws {
    // Cases with associated values cannot have raw values. Mixing them inside
    // a raw-value enum is invalid Swift; the refactor should bail rather than
    // produce broken output like `case payload = 0(String)`.
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        1️⃣enum Event: Int {
          case empty
          case payload(String)
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testAddExplicitEnumRawValuesUInt() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Mask: UInt {
        case bit02️⃣
        case bit13️⃣
        case bit2 = 4
        case bit34️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Add Explicit Raw Values",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = 0"),
                TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = 1"),
                TextEdit(range: positions["4️⃣"]..<positions["4️⃣"], newText: " = 5"),
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddExplicitEnumRawValuesNotOfferedForUnsignedEnumWithNegativeRawValue() async throws {
    // `case = -1` is invalid Swift for an unsigned-integer raw type, so the
    // refactor must not insert further values based on it.
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        1️⃣enum Mask: UInt {
          case a = -1
          case b
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testAddExplicitEnumRawValuesNotOfferedWhenEnumHasAttribute() async throws {
    // Attached attribute macros can introduce members at expansion time, so
    // the refactor bails whenever the enum has any attributes.
    try await assertNoCodeAction(
      titled: "Add Explicit Raw Values",
      in: """
        @available(*, deprecated)
        1️⃣enum Status: Int {
          case active
          case inactive
        }
        """,
      atMarker: "1️⃣"
    )
  }

  func testFormatRawStringLiteral() async throws {
    try await assertCodeActions(
      """
      let x = 1️⃣#"Hello 2️⃣world"#3️⃣
      """,
      ranges: [("1️⃣", "3️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert string literal to minimal number of \'#\'s",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: #""Hello world""#)]]
          )
        )
      ]
    }
  }

  func testFormatRawStringLiteralFromInterpolation() async throws {
    try await assertCodeActions(
      ##"""
      let x = 1️⃣#"Hello 2️⃣\#(name)"#3️⃣
      """##,
      ranges: [("1️⃣", "3️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert string literal to minimal number of \'#\'s",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["3️⃣"],
                  newText: ##"""
                    ##"Hello \#(name)"##
                    """##
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testFormatRawStringLiteralDoesNotShowUpWhenInvokedFromInsideInterpolationSegment() async throws {
    try await assertCodeActions(
      ##"""
      let x = #"Hello \#(n1️⃣ame)"#
      """##,
      ignoringCodeActions: ["Convert to computed property"]
    ) { uri, positions in
      []
    }
  }

  func testMigrateIfLetSyntax() async throws {
    try await assertCodeActions(
      ##"""
      1️⃣if 2️⃣let 3️⃣foo = 4️⃣foo {}5️⃣
      """##,
      markers: ["1️⃣", "2️⃣", "3️⃣", "4️⃣"],
      ranges: [("1️⃣", "4️⃣"), ("1️⃣", "5️⃣")]
    ) { uri, positions in
      [
        CodeAction(
          title: "Migrate to shorthand 'if let' syntax",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["5️⃣"],
                  newText: "if let foo {}"
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testMigrateIfLetSyntaxDoesNotShowUpWhenInvokedFromInsideTheBody() async throws {
    try await assertCodeActions(
      ##"""
      if let foo = foo 1️⃣{
        2️⃣print(foo)
      3️⃣}4️⃣
      """##
    ) { uri, positions in
      []
    }
  }

  func testOpaqueParameterToGeneric() async throws {
    try await assertCodeActions(
      ##"""
      1️⃣func 2️⃣someFunction(_ 3️⃣input: some4️⃣ Value) {}5️⃣
      """##,
      markers: ["1️⃣", "2️⃣", "3️⃣", "4️⃣"],
      ranges: [("1️⃣", "2️⃣"), ("1️⃣", "5️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Expand 'some' parameters to generic parameters",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["5️⃣"],
                  newText: "func someFunction<T1: Value>(_ input: T1) {}"
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testOpaqueParameterToGenericIsNotShownFromTheBody() async throws {
    try await assertCodeActions(
      ##"""
      func someFunction(_ input: some Value) 1️⃣{
        2️⃣print("x")
      }3️⃣
      """##,
      ignoringCodeActions: ["Add documentation"]
    ) { uri, positions in
      []
    }
  }

  func testConvertJSONToCodable() async throws {
    try await assertCodeActions(
      ##"""
      1️⃣{
        2️⃣"id": 3️⃣1,
        "values": 4️⃣["foo", "bar"]
      }5️⃣

      """##,
      ranges: [("1️⃣", "5️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Create Codable structs from JSON",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["5️⃣"],
                  newText: """
                    struct JSONValue: Codable {
                        var id: Double
                        var values: [String]
                    }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddDocumentationRefactorNotAtStartOfFile() async throws {
    try await assertCodeActions(
      """
      struct Foo {
        1️⃣func 2️⃣refactor(3️⃣syntax: 4️⃣Decl5️⃣Syntax)6️⃣ { }7️⃣
      }
      """,
      ranges: [("1️⃣", "2️⃣"), ("1️⃣", "6️⃣"), ("1️⃣", "7️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Add documentation",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: Range(positions["1️⃣"]),
                  newText: """
                    /// A description
                      /// - Parameter syntax:
                      \("")
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddDocumentationRefactorAtStartOfFile() async throws {
    try await assertCodeActions(
      """
      1️⃣func 2️⃣refactor(3️⃣syntax: 4️⃣Decl5️⃣Syntax)6️⃣ { }7️⃣
      """,
      ranges: [("1️⃣", "2️⃣"), ("1️⃣", "6️⃣"), ("1️⃣", "7️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Add documentation",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: Range(positions["1️⃣"]),
                  newText: """
                    /// A description
                    /// - Parameter syntax:
                    \("")
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testAddDocumentationDoesNotShowUpIfItIsNotOnItsOwnLine() async throws {
    try await assertCodeActions(
      """
      var x = 1; var 1️⃣y = 2
      """,
      ignoringCodeActions: ["Convert to computed property", "Convert Stored Property to Computed Property"]
    ) { uri, positions in
      []
    }
  }

  func testConvertStringConcatenationToStringInterpolation() async throws {
    try await assertCodeActions(
      #"""
      0️⃣
      1️⃣/*leading*/ #"["# + 2️⃣key + ": \(3️⃣d) " + 4️⃣value + ##"]"## /*trailing*/5️⃣
      """#,
      markers: ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"],
      ranges: [("1️⃣", "2️⃣"), ("3️⃣", "4️⃣"), ("1️⃣", "5️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert String Concatenation to String Interpolation",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["0️⃣"]..<positions["5️⃣"],
                  newText: ###"""

                    /*leading*/ ##"[\##(key): \##(d) \##(value)]"## /*trailing*/
                    """###
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertStringConcatenationToStringInterpolationWithInterspersingAndMultilineComments() async throws {
    try await assertCodeActions(
      """
      1️⃣"hello" + /*self.leading1*/   /**self.leading2*/   self   //self.trailing1
      ///concat.leading1
      2️⃣+/*concat.trailing1
      line 1
      line 2


      line 3
      */ value3️⃣
      """,
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert String Concatenation to String Interpolation",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["3️⃣"],
                  newText: #"""
                    "hello\(/*self.leading1*/ /**self.leading2*/ self /*self.trailing1*/ /**concat.leading1*/)\(/*concat.trailing1 line 1 line 2   line 3 */ value)"
                    """#
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertStringConcatenationToStringInterpolationNotShowUpMissingExpr() async throws {
    try await assertCodeActions(
      ###"""
      1️⃣"Hello" + 2️⃣
      """###,
      ranges: [("1️⃣", "2️⃣")]
    ) { uri, positions in
      []
    }
  }

  func testConvertStringConcatenationToStringInterpolationNotShowUpOnlyOneStringLiteral() async throws {
    try await assertCodeActions(
      ###"""
      1️⃣"[\(2️⃣key): \(3️⃣d) 4️⃣\(value)]"5️⃣
      """###,
      ranges: [("1️⃣", "2️⃣"), ("3️⃣", "4️⃣"), ("1️⃣", "5️⃣")]
    ) { uri, positions in
      []
    }
  }

  func testConvertStringConcatenationToStringInterpolationMultilineStringLiteral() async throws {
    try await assertCodeActions(
      ###"""
      1️⃣"""
      I
      am
      a
      """ + value +
      """
      multi-line
      string
      literal
      """2️⃣
      """###,
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert String Concatenation to String Interpolation",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["2️⃣"],
                  newText: #"""
                    """
                    I
                    am
                    a\(value)multi-line
                    string
                    literal
                    """
                    """#
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertStringConcatenationToStringInterpolationDifferentIndentation() async throws {
    try await assertCodeActions(
      ###"""
      1️⃣"""
      a
      b
      """ + """
        c
        d
        """2️⃣
      """###,
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert String Concatenation to String Interpolation",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["2️⃣"],
                  newText: #"""
                    """
                      a
                      bc
                      d
                      """
                    """#
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertStringConcatenationMixedMultilineAndSingleLine() async throws {
    // multiline + single-line + multiline
    try await assertCodeActions(
      ###"""
      1️⃣"""
      a
      b
      """ + "c" + """
      d
      """2️⃣
      """###,
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert String Concatenation to String Interpolation",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["2️⃣"],
                  newText: #"""
                    """
                    a
                    bcd
                    """
                    """#
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertStringConcatenationMultilinePlusSingleLine() async throws {
    // multiline + single-line ending
    try await assertCodeActions(
      ###"""
      1️⃣"""
      a
      b
      """ + "c"2️⃣
      """###,
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert String Concatenation to String Interpolation",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["2️⃣"],
                  newText: #"""
                    """
                    a
                    bc
                    """
                    """#
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testApplyDeMorganLawNegatedAnd() async throws {
    try await assertCodeActions(
      """
      let x = 1️⃣!(a && b)2️⃣
      """,
      ranges: [("1️⃣", "2️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Apply De Morgan's law, converting '!(a && b)' to '(!a || !b)'",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["2️⃣"],
                  newText: "(!a || !b)"
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testApplyDeMorganLawNegatedOr() throws {
    try assertDeMorganTransform(
      input: "!(a || b)",
      expected: "(!a && !b)"
    )
  }

  func testApplyDeMorganLawDoubleNegation() throws {
    try assertDeMorganTransform(
      input: "!(!a && !b)",
      expected: "(a || b)"
    )
  }

  func testApplyDeMorganLawComparisonFlip() throws {
    try assertDeMorganTransform(
      input: "!(a < b)",
      expected: "(a >= b)"
    )
  }

  func testApplyDeMorganLawPrecedencePreservation() throws {
    try assertDeMorganTransform(
      input: "!(a && b || c)",
      expected: "((!a || !b) && !c)"
    )
  }

  func testApplyDeMorganLawBitwise() throws {
    try assertDeMorganTransform(
      input: "~(a | b)",
      expected: "(~a & ~b)"
    )
  }

  func testApplyDeMorganLawPropositionsToNegation() throws {
    try assertDeMorganTransform(
      input: "!a || !b",
      expected: "!(a && b)"
    )
  }

  func testApplyDeMorganLawNestedNegation() throws {
    try assertDeMorganTransform(
      input: "!(!(a && b) || c)",
      expected: "((a && b) && !c)"
    )
  }

  func testApplyDeMorganLawOrToAnd() throws {
    try assertDeMorganTransform(
      input: "!((a || b) && c)",
      expected: "((!a && !b) || !c)"
    )
  }

  func testApplyDeMorganLawTernaryPropagation() throws {
    try assertDeMorganTransform(
      input: "!(a ? !b : c)",
      expected: "(a ? b : !c)"
    )
  }

  func testApplyDeMorganLawWithIsExpression() throws {
    try assertDeMorganTransform(
      input: "!a || !(s is String)",
      expected: "!(a && (s is String))"
    )
  }

  func testApplyDeMorganLawReducedBoolean() throws {
    try assertDeMorganTransform(
      input: "((((((a !== !(b || c)))) && !d)))",
      expected: "!((((((a === !(b || c)))) || d)))"
    )
  }

  func testApplyDeMorganLawReducedBooleanNonNested() throws {
    try assertDeMorganTransform(
      input: "!a || ((!((b)))) || s is String || c != d",
      expected: "!(a && ((((b)))) && !(s is String) && c == d)"
    )
  }

  func testApplyDeMorganLawSpreadBitwise() throws {
    try assertDeMorganTransform(
      input: "~((b | ((c)) | d | e & ~f | (~g & h)))",
      expected: "((~b & ~((c)) & ~d & (~e | f) & (g | ~h)))"
    )
  }

  func testApplyDeMorganLawTernaryExpansion() throws {
    try assertDeMorganTransform(
      input: "!((a ? b : !c) || (!d ? !e : f) && (g ? h : i))",
      expected: "((a ? !b : c) && ((!d ? e : !f) || !(g ? h : i)))"
    )
  }

  func testApplyDeMorganLawTernaryNoPropagation() throws {
    // Negating the ternary (b ? !c : !d) adds complexity (2 negations) vs wrapping (1 negation).
    // So we expect the ternary to be wrapped in parens and negated.
    try assertDeMorganTransform(
      input: "!(a && (b ? c : d))",
      expected: "(!a || !(b ? c : d))"
    )
  }

  func testApplyDeMorganLawBooleanLiteral() throws {
    // !true -> false
    try assertDeMorganTransform(
      input: "!true",
      expected: "false"
    )

    // !(a && false) -> !a || true
    try assertDeMorganTransform(
      input: "!(a && false)",
      expected: "(!a || true)"
    )
  }

  func testApplyDeMorganLawTrivia() throws {
    // /*c1*/!(/*c2*/a /*c3*/&& /*c4*/b/*c5*/)/*c6*/
    // Note: Comments attached to the removed '!' (/*c1*/) are dropped.
    // Comment between '!' and '(' is illegal in swift.
    try assertDeMorganTransform(
      input: "/*c1*/!(/*c2*/a /*c3*/&& /*c4*/b/*c5*/)/*c6*/",
      expected: "(/*c2*/!a /*c3*/|| /*c4*/!b/*c5*/)/*c6*/"
    )
  }

  func testApplyDeMorganLawAdvancedTrivia() throws {
    // Multiline preservation
    try assertDeMorganTransform(
      input: """
        !(
          a
          &&
          b
        )
        """,
      expected: """
        (
          !a
          ||
          !b
        )
        """
    )

    // Line comments
    try assertDeMorganTransform(
      input: """
        !(a && b // check
        )
        """,
      expected: """
        (!a || !b // check
        )
        """
    )

    // Comments attached to inner operators
    try assertDeMorganTransform(
      input: "!(a /*op*/ && b)",
      expected: "(!a /*op*/ || !b)"
    )
  }

  func testApplyDeMorganLawTryNegationPropagation() throws {
    try assertDeMorganTransform(
      input: "true && try a",
      expected: "!(false || try !a)"
    )
  }

  func testApplyDeMorganLawAwaitNegationPropagation() throws {
    try assertDeMorganTransform(
      input: "true && await a",
      expected: "!(false || await !a)"
    )
  }

  func testApplyDeMorganLawTryOptionalDoesNotPropagate() throws {
    try assertDeMorganTransform(
      input: "true && try? a",
      expected: "!(false || !(try? a))"
    )
  }

  func testApplyDeMorganLawTryWithTrivia() throws {
    try assertDeMorganTransform(
      input: "true && try /* comment */ a",
      expected: "!(false || try /* comment */ !a)"
    )
  }

  func testApplyDeMorganLawTryFusionCheck() throws {
    try assertDeMorganTransform(
      input: "true && try(a)",
      expected: "!(false || try !(a))"
    )
  }

  func testApplyDeMorganLawForcedUnwrapAsAtomic() throws {
    try assertDeMorganTransform(
      input: "a! && false",
      expected: "!(!a! || true)"
    )
  }

  func testApplyDeMorganLawTriviaPreservation() throws {
    try assertDeMorganTransform(
      input: "true /*a*/&& false",
      expected: "!(false /*a*/|| true)"
    )
  }

  func testApplyDeMorganLawPreservesIndentationForPropositions() throws {
    try assertDeMorganTransform(
      input: "  !a || !b",
      expected: "  !(a && b)"
    )
  }

  func testApplyDeMorganLawPreservesTriviaForPropositions() throws {
    try assertDeMorganTransform(
      input: "/* c */ !a || !b",
      expected: "/* c */ !(a && b)"
    )
  }

  func testApplyDeMorganLawNestedActionAvailability() async throws {
    try await assertCodeActions(
      """
      let x = 1️⃣!2️⃣(3️⃣!(4️⃣a && b) 5️⃣|| c6️⃣)7️⃣
      """,
      markers: ["4️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Apply De Morgan's law, converting '!(a && b) ' to '(!a || !b) '",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["3️⃣"]..<positions["5️⃣"],
                  newText: "(!a || !b) "
                )
              ]
            ]
          )
        ),
        CodeAction(
          title: "Apply De Morgan's law, converting '!(a && b) || c' to '!((a && b) && !c)'",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["3️⃣"]..<positions["6️⃣"],
                  newText: "!((a && b) && !c)"
                )
              ]
            ]
          )
        ),
        CodeAction(
          title: "Apply De Morgan's law, converting '(!(a && b) || c)' to '!((a && b) && !c)'",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["7️⃣"],
                  newText: "!((a && b) && !c)"
                )
              ]
            ]
          )
        ),
        CodeAction(
          title: "Apply De Morgan's law, converting '!(!(a && b) || c)' to '((a && b) && !c)'",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["7️⃣"],
                  newText: "((a && b) && !c)"
                )
              ]
            ]
          )
        ),
      ]
    }
  }

  func testAddMissingImport() async throws {
    let project = try await SwiftPMTestProject(
      files: [
        "MyLibrary/ImportedType.swift": """
        public struct ImportedType {
        public init() {}
        }
        """,
        "MyApp/main.swift": """
        let value = 1️⃣ImportedType()
        """,
      ],
      manifest: """
        let package = Package(
          name: "MissingImport",
          targets: [
            .target(name: "MyLibrary"),
            .executableTarget(name: "MyApp", dependencies: ["MyLibrary"]),
          ]
        )
        """,
      capabilities: clientCapabilitiesWithCodeActionSupport,
      enableBackgroundIndexing: true
    )

    let (uri, positions) = try project.openDocument("main.swift")

    let diagnosticReport = try await project.testClient.send(
      DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
    )

    let diagnostics = try XCTUnwrap(diagnosticReport.fullReport?.items)

    let result = try await project.testClient.send(
      CodeActionRequest(
        range: Range(positions["1️⃣"]),
        context: CodeActionContext(
          diagnostics: diagnostics,
          only: [.quickFix],
          triggerKind: .invoked
        ),
        textDocument: TextDocumentIdentifier(uri)
      )
    )

    let codeActions = try XCTUnwrap(result?.codeActions)
    let actionTitles = codeActions.map(\.title)
    let addImportAction = try XCTUnwrap(
      codeActions.first { $0.title == "Add import for 'MyLibrary'" },
      "Expected Add import for 'MyLibrary' action. Available actions: \(actionTitles). Diagnostics: \(diagnostics.map { "\($0.source ?? "nil"): \($0.message)" })"
    )

    XCTAssertEqual(addImportAction.kind, .quickFix)
    XCTAssertEqual(
      addImportAction.edit?.changes?[uri],
      [
        TextEdit(
          range: Position(line: 0, utf16index: 0)..<Position(line: 0, utf16index: 0),
          newText: "import MyLibrary\n\n"
        )
      ]
    )
  }

  func testAddMissingImportNotAvailableForAmbiguousSymbol() async throws {
    let project = try await SwiftPMTestProject(
      files: [
        "LibA/ImportedType.swift": """
        public struct ImportedType {
        public init() {}
        }
        """,
        "LibB/ImportedType.swift": """
        public struct ImportedType {
        public init() {}
        }
        """,
        "MyApp/main.swift": """
        let value = 1️⃣ImportedType()
        """,
      ],
      manifest: """
        let package = Package(
          name: "MissingImport",
          targets: [
            .target(name: "LibA"),
            .target(name: "LibB"),
            .executableTarget(name: "MyApp", dependencies: ["LibA", "LibB"]),
          ]
        )
        """,
      capabilities: clientCapabilitiesWithCodeActionSupport,
      enableBackgroundIndexing: true
    )

    let (uri, positions) = try project.openDocument("main.swift")

    let diagnosticReport = try await project.testClient.send(
      DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
    )

    let diagnostics = try XCTUnwrap(diagnosticReport.fullReport?.items)

    let result = try await project.testClient.send(
      CodeActionRequest(
        range: Range(positions["1️⃣"]),
        context: CodeActionContext(
          diagnostics: diagnostics,
          only: [.quickFix],
          triggerKind: .invoked
        ),
        textDocument: TextDocumentIdentifier(uri)
      )
    )

    let codeActions = try XCTUnwrap(result?.codeActions)
    XCTAssertFalse(codeActions.contains { $0.title.hasPrefix("Add import for ") })
  }

  func testAddMissingImportInsertsAtStartOfFileBeforeDocComment() async throws {
    let project = try await SwiftPMTestProject(
      files: [
        "MyLibrary/ImportedType.swift": """
        public struct ImportedType {
          public init() {}
        }
        """,
        "MyApp/main.swift": """
        /// Do an amazing thing
        struct Starstruck {
          let value = 1️⃣ImportedType()
        }
        """,
      ],
      manifest: """
        let package = Package(
          name: "MissingImport",
          targets: [
            .target(name: "MyLibrary"),
            .executableTarget(name: "MyApp", dependencies: ["MyLibrary"]),
          ]
        )
        """,
      capabilities: clientCapabilitiesWithCodeActionSupport,
      enableBackgroundIndexing: true
    )

    let (uri, positions) = try project.openDocument("main.swift")

    let diagnosticReport = try await project.testClient.send(
      DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
    )
    let diagnostics = try XCTUnwrap(diagnosticReport.fullReport?.items)

    let result = try await project.testClient.send(
      CodeActionRequest(
        range: Range(positions["1️⃣"]),
        context: CodeActionContext(
          diagnostics: diagnostics,
          only: [.quickFix],
          triggerKind: .invoked
        ),
        textDocument: TextDocumentIdentifier(uri)
      )
    )

    let codeActions = try XCTUnwrap(result?.codeActions)
    let addImportAction = try XCTUnwrap(
      codeActions.first { $0.title == "Add import for 'MyLibrary'" },
      "Expected Add import for 'MyLibrary' action. Available actions: \(codeActions.map(\.title))"
    )

    XCTAssertEqual(
      addImportAction.edit?.changes?[uri],
      [
        TextEdit(
          range: Position(line: 0, utf16index: 0)..<Position(line: 0, utf16index: 0),
          newText: "import MyLibrary\n\n"
        )
      ]
    )
  }

  func testRemoveUnusedImports() async throws {
    let project = try await SwiftPMTestProject(
      files: [
        "LibA/LibA.swift": "",
        "LibB/LibB.swift": "",
        "Test/Test.swift": """
        // Some file header
        // over multiple lines

        1️⃣import LibA // LibA implements A
        2️⃣import Foundation3️⃣
        // LibB implements B
        import LibB4️⃣

        #warning("Removing imports should work despite warning")
        5️⃣func test(x: Date) {}
        """,
      ],
      manifest: """
        let package = Package(
          name: "MyLibrary",
          targets: [
            .target(name: "LibA"),
            .target(name: "LibB"),
            .target(
              name: "Test",
              dependencies: ["LibA", "LibB"],
              swiftSettings: [.enableUpcomingFeature("MemberImportVisibility")]
            )
          ]
        )
        """,
      capabilities: clientCapabilitiesWithCodeActionSupport,
      enableBackgroundIndexing: true
    )

    let (uri, positions) = try project.openDocument("Test.swift")

    let functionResult = try await project.testClient.send(
      CodeActionRequest(
        range: Range(positions["5️⃣"]),
        context: CodeActionContext(),
        textDocument: TextDocumentIdentifier(uri)
      )
    )
    XCTAssertFalse(
      try XCTUnwrap(functionResult?.codeActions).contains(where: {
        $0.command?.command == RemoveUnusedImportsCommand.identifier
      })
    )

    let importResult = try await project.testClient.send(
      CodeActionRequest(
        range: Range(positions["1️⃣"]),
        context: CodeActionContext(),
        textDocument: TextDocumentIdentifier(uri)
      )
    )
    let removeUnusedImportsCommand = try XCTUnwrap(
      importResult?.codeActions?.first(where: {
        $0.command?.command == "remove.unused.imports.command"
      })?.command
    )

    project.testClient.handleSingleRequest { (request: ApplyEditRequest) -> ApplyEditResponse in
      XCTAssertEqual(
        request.edit.changes,
        [
          uri: [
            TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: ""),
            TextEdit(range: positions["3️⃣"]..<positions["4️⃣"], newText: ""),
          ]
        ]
      )
      return ApplyEditResponse(applied: true, failureReason: nil)
    }

    _ = try await project.testClient.send(
      ExecuteCommandRequest(
        command: removeUnusedImportsCommand.command,
        arguments: removeUnusedImportsCommand.arguments
      )
    )
  }

  func testRemoveUnusedImportsNotAvailableIfSourceFileHasError() async throws {
    let project = try await SwiftPMTestProject(
      files: [
        "Test.swift": """
        1️⃣import Foundation

        #error("Some error")
        """
      ],
      manifest: """
        let package = Package(
          name: "MyLibrary",
          targets: [
            .target(name: "Test", swiftSettings: [.enableUpcomingFeature("MemberImportVisibility")]c)
          ]
        )
        """,
      capabilities: clientCapabilitiesWithCodeActionSupport,
      enableBackgroundIndexing: true
    )

    let (uri, positions) = try project.openDocument("Test.swift")

    let result = try await project.testClient.send(
      CodeActionRequest(
        range: Range(positions["1️⃣"]),
        context: CodeActionContext(),
        textDocument: TextDocumentIdentifier(uri)
      )
    )
    XCTAssertFalse(
      try XCTUnwrap(result?.codeActions).contains(where: {
        $0.command?.command == RemoveUnusedImportsCommand.identifier
      })
    )
  }

  func testConvertFunctionZeroParameterToComputedProperty() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)

    let positions = testClient.openDocument(
      """
      1️⃣func someFunction() -> String2️⃣ { return "" }3️⃣
      """,
      uri: uri
    )

    let request = CodeActionRequest(
      range: positions["1️⃣"]..<positions["2️⃣"],
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)

    guard case .codeActions(let codeActions) = result else {
      XCTFail("Expected code actions")
      return
    }

    let expectedCodeAction = CodeAction(
      title: "Convert to computed property",
      kind: .refactorInline,
      diagnostics: nil,
      edit: WorkspaceEdit(
        changes: [
          uri: [
            TextEdit(
              range: positions["1️⃣"]..<positions["3️⃣"],
              newText: """
                var someFunction: String { return "" }
                """
            )
          ]
        ]
      )
    )

    XCTAssertTrue(codeActions.contains(expectedCodeAction))
  }

  func testConvertZeroParameterFunctionToComputedPropertyIsNotShownFromTheBody() async throws {
    try await assertCodeActions(
      ##"""
      func someFunction() -> String 1️⃣{
        2️⃣return ""
      }3️⃣
      """##,
      ignoringCodeActions: ["Add documentation"]
    ) { uri, positions in
      []
    }
  }

  func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForImplicitVoid() async throws {
    try await assertCodeActions(
      """
      1️⃣func test()2️⃣ { }3️⃣
      """,
      ranges: [("1️⃣", "2️⃣")],
      ignoringCodeActions: ["Add documentation"]
    ) { _, _ in
      []
    }
  }

  func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForExplicitVoid() async throws {
    try await assertCodeActions(
      """
      1️⃣func test() -> Void2️⃣ { }3️⃣
      """,
      ranges: [("1️⃣", "2️⃣")],
      ignoringCodeActions: ["Add documentation"]
    ) { _, _ in
      []
    }
  }

  func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForEmptyTuple() async throws {
    try await assertCodeActions(
      """
      1️⃣func test() -> ()2️⃣ { }3️⃣
      """,
      ranges: [("1️⃣", "2️⃣")],
      ignoringCodeActions: ["Add documentation"]
    ) { _, _ in
      []
    }
  }

  func testConvertComputedPropertyToZeroParameterFunction() async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift)

    let positions = testClient.openDocument(
      """
      1️⃣var someFunction: String2️⃣ { return "" }3️⃣
      """,
      uri: uri
    )

    let request = CodeActionRequest(
      range: positions["1️⃣"]..<positions["2️⃣"],
      context: .init(),
      textDocument: TextDocumentIdentifier(uri)
    )
    let result = try await testClient.send(request)

    guard case .codeActions(let codeActions) = result else {
      XCTFail("Expected code actions")
      return
    }

    let expectedCodeAction = CodeAction(
      title: "Convert to zero parameter function",
      kind: .refactorInline,
      diagnostics: nil,
      edit: WorkspaceEdit(
        changes: [
          uri: [
            TextEdit(
              range: positions["1️⃣"]..<positions["3️⃣"],
              newText: """
                func someFunction() -> String { return "" }
                """
            )
          ]
        ]
      )
    )

    XCTAssertTrue(codeActions.contains(expectedCodeAction))
  }

  func testConvertComputedPropertyToZeroParameterFunctionIsNotShownFromTheBody() async throws {
    try await assertCodeActions(
      ##"""
      var someFunction: String {
        1️⃣return ""
      }
      """##
    ) { uri, positions in
      []
    }
  }

  func testConvertComputeToStoredProperty() async throws {
    try await assertCodeActions(
      ##"""
      1️⃣var 2️⃣foo: String { "abc" }3️⃣
      """##,
      markers: ["2️⃣"],
      ignoringCodeActions: ["Add documentation", "Convert to zero parameter function"]
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert to stored property",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["3️⃣"],
                  newText: """
                    let foo: String = "abc"
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertLineCommentToDocComment() async throws {
    try await assertCodeActions(
      """
      1️⃣// A comment
      func foo() {}2️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert Comment to Doc Comment",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["2️⃣"],
                  newText: """
                    /// A comment
                    func foo() {}
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertBlockCommentToDocComment() async throws {
    try await assertCodeActions(
      """
      1️⃣/* A block comment */
      func foo() {}2️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert Comment to Doc Comment",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["2️⃣"],
                  newText: """
                    /** A block comment */
                    func foo() {}
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertMultipleLineCommentsToDocComments() async throws {
    try await assertCodeActions(
      """
      1️⃣// First line
      // Second line
      func foo() {}2️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert Comment to Doc Comment",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["1️⃣"]..<positions["2️⃣"],
                  newText: """
                    /// First line
                    /// Second line
                    func foo() {}
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertCommentToDocCommentNotShownOnFunctionBody() async throws {
    try await assertCodeActions(
      """
      // A comment
      func foo() {
        1️⃣print("hello")
      }
      """
    ) { uri, positions in
      []
    }
  }

  func testSwapBinaryOperandsComparison() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣1 1️⃣<2️⃣ 23️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["0️⃣"]..<positions["3️⃣"], newText: "2 > 1")
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsCommutative() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣a 1️⃣+2️⃣ b3️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["0️⃣"]..<positions["3️⃣"], newText: "b + a")
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsNotOfferedOutsideOperator() async throws {
    try await assertNoCodeAction(
      titled: "Swap operands",
      in: """
        let x = 1️⃣a + b
        """,
      atMarker: "1️⃣"
    )
  }

  func testSwapBinaryOperandsNotOfferedForUnsupportedOperator() async throws {
    try await assertNoCodeAction(
      titled: "Swap operands",
      in: """
        let x = a 1️⃣- b
        """,
      atMarker: "1️⃣"
    )
  }

  func testSwapBinaryOperandsNestedExpression() async throws {
    try await assertCodeActions(
      """
      let x = 1 + 0️⃣-2 1️⃣*2️⃣ 53️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["0️⃣"]..<positions["3️⃣"],
                  newText: "5 * -2"
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsLessThanOrEqual() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣a 1️⃣<=2️⃣ b3️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["0️⃣"]..<positions["3️⃣"], newText: "b >= a")
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsNotOfferedOutsideOperatorRight() async throws {
    try await assertNoCodeAction(
      titled: "Swap operands",
      in: """
        let x = a + 1️⃣b
        """,
      atMarker: "1️⃣"
    )
  }

  func testSwapBinaryOperandsGreaterThan() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣a 1️⃣>2️⃣ b3️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["0️⃣"]..<positions["3️⃣"], newText: "b < a")
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsEquality() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣a 1️⃣==2️⃣ b3️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["0️⃣"]..<positions["3️⃣"], newText: "b == a")
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsPlusOperatorInNestedExpression() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣1 1️⃣+2️⃣ -2 * 53️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["0️⃣"]..<positions["3️⃣"],
                  newText: "-2 * 5 + 1"
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsNotOfferedOnNestedOperand() async throws {
    try await assertNoCodeAction(
      titled: "Swap operands",
      in: """
        let x = 1 + 1️⃣-2 * 5
        """,
      atMarker: "1️⃣"
    )
  }

  func testSwapBinaryOperandsNotEqual() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣a 1️⃣!=2️⃣ b3️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["0️⃣"]..<positions["3️⃣"], newText: "b != a")
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsNotOfferedWhitespaceBeforeOperator() async throws {
    try await assertNoCodeAction(
      titled: "Swap operands",
      in: """
        let x = a1️⃣ + b
        """,
      atMarker: "1️⃣"
    )
  }

  func testSwapBinaryOperandsOfferedWhitespaceAfterOperator() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣a +1️⃣ b3️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["0️⃣"]..<positions["3️⃣"], newText: "b + a")
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsPreservesTrivia() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣a /* left */ 1️⃣+2️⃣ /* right */ b3️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(range: positions["0️⃣"]..<positions["3️⃣"], newText: "b /* left */ + /* right */ a")
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsMultiline() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣a
        1️⃣+2️⃣ b3️⃣
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["0️⃣"]..<positions["3️⃣"],
                  newText: "b\n  + a"
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsNotOfferedForIncompleteExpressionRightMissing() async throws {
    try await assertNoCodeAction(
      titled: "Swap operands",
      in: """
        let x = a 1️⃣+
        """,
      atMarker: "1️⃣"
    )
  }

  func testSwapBinaryOperandsNotOfferedForIncompleteExpressionLeftMissing() async throws {
    try await assertNoCodeAction(
      titled: "Swap operands",
      in: """
        let x = 1️⃣+ b
        """,
      atMarker: "1️⃣"
    )
  }

  func testSwapBinaryOperandsMultipleOperators() async throws {
    try await assertCodeActions(
      """
      let x = 0️⃣1 1️⃣+2️⃣ 2 3️⃣- 3
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Swap operands",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["0️⃣"]..<positions["3️⃣"],
                  newText: "2 + 1 "
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testSwapBinaryOperandsNotOfferedForFunctionArgument() async throws {
    try await assertNoCodeAction(
      titled: "Swap operands",
      in: """
        let x = 2 * list.reduce(0, 1️⃣+)
        """,
      atMarker: "1️⃣"
    )
  }

  func testGenerateEnumCaseAsAccessors() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case text(String)
          case number(Int)
          case unknown2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'as' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var asText: String? {
                            if case let .text(value) = self { return value }
                            return nil
                        }

                        var asNumber: Int? {
                            if case let .number(value) = self { return value }
                            return nil
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseIsAccessors() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case text(String)
          case number(Int)
          case unknown2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'is' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var isText: Bool {
                            if case .text = self { return true }
                            return false
                        }

                        var isNumber: Bool {
                            if case .number = self { return true }
                            return false
                        }

                        var isUnknown: Bool {
                            if case .unknown = self { return true }
                            return false
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseAsAccessorsForMultipleAssociatedValues() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case pair(Int, String)2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'as' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var asPair: (Int, String)? {
                            if case let .pair(value1, value2) = self { return (value1, value2) }
                            return nil
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseAsAccessorsForLabeledValues() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case point(x: Int, y: Int)
          case single(Int)2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'as' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var asPoint: (x: Int, y: Int)? {
                            if case let .point(x, y) = self { return (x, y) }
                            return nil
                        }

                        var asSingle: Int? {
                            if case let .single(value) = self { return value }
                            return nil
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseIsAccessorsSkipsExistingMember() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case text(String)
          case number(Int)

          var isText: Bool { false }2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'is' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var isNumber: Bool {
                            if case .number = self { return true }
                            return false
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseIsAccessorsNotOfferedWhenAllAccessorsExist() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case text
          case number

          var isText: Bool { false }
          var isNumber: Bool { false }
      }
      """,
      markers: ["1️⃣"],
      ignoringCodeActions: ["Add documentation"],
      exhaustive: true
    ) { _, _ in
      []
    }
  }

  func testGenerateEnumCaseAsAccessorsSkipsExistingMember() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case text(String)
          case number(Int)

          var asText: String? { nil }2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'as' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var asNumber: Int? {
                            if case let .number(value) = self { return value }
                            return nil
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseAsAccessorsForFunctionTypedValue() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case handler((Int) -> Void)2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'as' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var asHandler: ((Int) -> Void)? {
                            if case let .handler(value) = self { return value }
                            return nil
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseAsAccessorsInfersIndentation() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
        case text(String)
        case number(Int)
        case other(Bool)2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'as' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                      var asText: String? {
                        if case let .text(value) = self { return value }
                        return nil
                      }

                      var asNumber: Int? {
                        if case let .number(value) = self { return value }
                        return nil
                      }

                      var asOther: Bool? {
                        if case let .other(value) = self { return value }
                        return nil
                      }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseIsAccessorsGeneratesDuplicateNames() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Value {
          case foo
          case Foo2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'is' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var isFoo: Bool {
                            if case .foo = self { return true }
                            return false
                        }

                        var isFoo: Bool {
                            if case .Foo = self { return true }
                            return false
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testGenerateEnumCaseAsAccessorsForOverloadedCaseNames() async throws {
    try await assertCodeActions(
      """
      1️⃣enum Foo {
          case value(int: Int)
          case value(string: String)2️⃣
      }
      """,
      markers: ["1️⃣"],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Generate 'as' accessors for enum cases",
          kind: .refactorInline,
          diagnostics: nil,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: positions["2️⃣"]..<positions["2️⃣"],
                  newText: """


                        var asValue: Int? {
                            if case let .value(int) = self { return int }
                            return nil
                        }

                        var asValue: String? {
                            if case let .value(string) = self { return string }
                            return nil
                        }
                    """
                )
              ]
            ]
          )
        )
      ]
    }
  }

  /// Retrieves the code action at a set of markers and asserts that it matches a list of expected code actions.
  ///
  /// - Parameters:
  ///   - markedText: The source file input to get the code actions for.
  ///   - markers: The list of markers to retrieve code actions at. If `nil` code actions will be retrieved for all
  ///     markers in `markedText`
  ///   - ranges: If specified, code actions are also requested for selection ranges between these markers.
  ///   - ignoringCodeActions: Ignore code actions with this title for exhaustiveness checking
  ///   - exhaustive: Whether `expected` is expected to be a subset of the returned code actions or whether it is
  ///     expected to exhaustively match all code actions.
  ///   - expected: A closure that returns the list of expected code actions, given the URI of the test document and the
  ///     marker positions within.
  private func assertCodeActions(
    _ markedText: String,
    markers: [String]? = nil,
    ranges: [(String, String)] = [],
    ignoringCodeActions: [String] = [],
    exhaustive: Bool = true,
    expected: (_ uri: DocumentURI, _ positions: DocumentPositions) -> [CodeAction],
    testName: String = #function,
    file: StaticString = #filePath,
    line: UInt = #line
  ) async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift, testName: testName)
    let positions = testClient.openDocument(markedText, uri: uri)

    var ranges = ranges
    if let markers {
      ranges += markers.map { ($0, $0) }
    } else {
      ranges += extractMarkers(markedText).markers.map(\.key).map { ($0, $0) }
    }

    for (startMarker, endMarker) in ranges {
      let result = try await testClient.send(
        CodeActionRequest(
          range: positions[startMarker]..<positions[endMarker],
          context: .init(),
          textDocument: TextDocumentIdentifier(uri)
        )
      )
      let codeActions = try XCTUnwrap(result?.codeActions, file: file, line: line).filter {
        !ignoringCodeActions.contains($0.title)
      }
      let expected = expected(uri, positions)
      if exhaustive {
        XCTAssertEqual(
          codeActions,
          expected,
          "Found unexpected code actions at range \(startMarker)-\(endMarker)",
          file: file,
          line: line
        )
      } else {
        if expected.isEmpty {
          XCTFail(
            "exhaustive: false is incompatible with an empty list of expected code actions",
            file: file,
            line: line
          )
        }
        XCTAssert(
          Set(codeActions).isSuperset(of: expected),
          """
          Code actions did not contain expected at range \(startMarker)-\(endMarker):
          \(codeActions)
          """,
          file: file,
          line: line
        )
      }
    }
  }

  /// Assert that no code action with the given title is offered at the given marker.
  private func assertNoCodeAction(
    titled title: String,
    in markedText: String,
    atMarker marker: String,
    testName: String = #function,
    file: StaticString = #filePath,
    line: UInt = #line
  ) async throws {
    let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
    let uri = DocumentURI(for: .swift, testName: testName)
    let positions = testClient.openDocument(markedText, uri: uri)
    let position = positions[marker]
    let result = try await testClient.send(
      CodeActionRequest(
        range: position..<position,
        context: .init(),
        textDocument: TextDocumentIdentifier(uri)
      )
    )
    let codeActions = result?.codeActions ?? []
    XCTAssertFalse(
      codeActions.contains(where: { $0.title == title }),
      "did not expect code action titled '\(title)' at marker \(marker), got: \(codeActions.map(\.title))",
      file: file,
      line: line
    )
  }

  func testConvertStoredPropertyToComputedWithTypeAnnotation() async throws {
    try await assertCodeActions(
      """
      struct S {
          1️⃣var x: Int = 252️⃣
      }
      """,
      markers: ["1️⃣"],
      ranges: [("1️⃣", "2️⃣")],
      exhaustive: false
    ) { uri, positions in
      [
        CodeAction(
          title: "Convert Stored Property to Computed Property",
          kind: .refactorInline,
          edit: WorkspaceEdit(
            changes: [
              uri: [
                TextEdit(
                  range: Position(line: 0, utf16index: 10)..<Position(line: 1, utf16index: 19),
                  newText: "\n    var x: Int { 25 }"
                )
              ]
            ]
          )
        )
      ]
    }
  }

  func testConvertStoredPropertyToComputedWithoutTypeAnnotationWithoutResolveSupport() async throws {
    try await assertCodeActions(
      """
      struct S {1️⃣
        2️⃣var x = 253️⃣
      }
      """,
      markers: ["2️⃣"],
      exhaustive: false,
      expected: { uri, positions in
        [
          CodeAction(
            title: "Convert Stored Property to Computed Property",
            kind: .refactorInline,
            edit: WorkspaceEdit(
              changes: [
                uri: [
                  TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "\n  var x: <#Type#>{ 25 }")
                ]
              ],
            )
          )
        ]
      }
    )
  }

  func testConvertStoredPropertyToComputedWithoutTypeAnnotationWithResolveSupport() async throws {
    var capabilities = clientCapabilitiesWithCodeActionSupport
    capabilities.textDocument?.codeAction?.resolveSupport = .init(properties: ["edit"])
    let testClient = try await TestSourceKitLSPClient(capabilities: capabilities)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      struct S {1️⃣
        2️⃣var x = 253️⃣
      }
      """,
      uri: uri
    )
    let actions = try await testClient.send(
      CodeActionRequest(
        range: positions["2️⃣"]..<positions["3️⃣"],
        context: CodeActionContext(),
        textDocument: TextDocumentIdentifier(uri)
      )
    )
    let convertAction = try XCTUnwrap(
      actions?.codeActions?.filter { $0.title == "Convert Stored Property to Computed Property" }.only
    )
    XCTAssertNil(convertAction.edit)

    let resolved = try await testClient.send(CodeActionResolveRequest(codeAction: convertAction))
    XCTAssertEqual(
      resolved.edit,
      WorkspaceEdit(
        changes: [
          uri: [
            TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "\n  var x: Int{ 25 }")
          ]
        ],
      )
    )
  }

  func testConvertStoredPropertyToComputedWithoutTypeAnnotationButAmbiguousTypeAndResolveSupport() async throws {
    var capabilities = clientCapabilitiesWithCodeActionSupport
    capabilities.textDocument?.codeAction?.resolveSupport = .init(properties: ["edit"])
    let testClient = try await TestSourceKitLSPClient(capabilities: capabilities)
    let uri = DocumentURI(for: .swift)
    let positions = testClient.openDocument(
      """
      struct S {1️⃣
        2️⃣var x = invalid3️⃣
      }
      """,
      uri: uri
    )
    let actions = try await testClient.send(
      CodeActionRequest(
        range: positions["2️⃣"]..<positions["3️⃣"],
        context: CodeActionContext(),
        textDocument: TextDocumentIdentifier(uri)
      )
    )
    let convertAction = try XCTUnwrap(
      actions?.codeActions?.filter { $0.title == "Convert Stored Property to Computed Property" }.only
    )
    XCTAssertNil(convertAction.edit)

    let resolved = try await testClient.send(CodeActionResolveRequest(codeAction: convertAction))
    XCTAssertEqual(
      resolved.edit,
      WorkspaceEdit(
        changes: [
          uri: [
            TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "\n  var x: <#Type#>{ invalid }")
          ]
        ],
      )
    )
  }

}

private extension CodeActionRequestResponse {
  var codeActions: [CodeAction]? {
    guard case .codeActions(let actions) = self else {
      return nil
    }
    return actions
  }
}

/// Tests De Morgan transformation directly without LSP overhead.
private func assertDeMorganTransform(
  input: String,
  expected: String,
  file: StaticString = #filePath,
  line: UInt = #line
) throws {
  let expr = ExprSyntax("\(raw: input)")

  let transformer = DeMorganTransformer()
  guard let result = transformer.computeComplement(of: expr) else {
    XCTFail("Failed to compute De Morgan complement", file: file, line: line)
    return
  }

  XCTAssertEqual(result.description, expected, file: file, line: line)
}
