//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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
//
//===----------------------------------------------------------------------===//

#if !NO_SWIFTPM_DEPENDENCY
@_spi(SourceKitLSP) import BuildServerProtocol
@_spi(Testing) import BuildServerIntegration
@_spi(SourceKitLSP) import LanguageServerProtocol
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
@_spi(SourceKitLSP) import LanguageServerProtocolTransport
import PackageModel
import SKLogging
import SKOptions
import SKTestSupport
import SourceKitLSP
@preconcurrency import SPMBuildCore
import SwiftExtensions
import TSCBasic
import TSCExtensions
import ToolchainRegistry
import Foundation
import Testing
import struct Basics.AbsolutePath
import struct Basics.Triple
import struct Basics.UniversalArchiver

private var hostTriple: Triple {
  get async throws {
    let toolchain = try #require(
      await ToolchainRegistry.forTesting.preferredToolchain(containing: [
        \.clang, \.clangd, \.sourcekitd, \.swift, \.swiftc,
      ])
    )
    let destinationToolchainBinDir = try #require(toolchain.swiftc?.deletingLastPathComponent())

    let hostSDK = try SwiftSDK.hostSwiftSDK(Basics.AbsolutePath(validating: destinationToolchainBinDir.filePath))
    let hostSwiftPMToolchain = try UserToolchain(swiftSDK: hostSDK)

    return hostSwiftPMToolchain.targetTriple
  }
}

fileprivate extension SourceKitLSPOptions {
  static var forTestingNativeSwiftPMBuildServer: Self {
    SourceKitLSPOptions(swiftPM: SwiftPMOptions(buildSystem: .native))
  }

  static var forTestingExperimentalSwiftPMBuildServer: Self {
    SourceKitLSPOptions(swiftPM: SwiftPMOptions(buildSystem: .swiftbuild))
  }
}

/// Compute a value that is derived from the toolchain synchronously.
///
/// The build servers that a test runs against are listed in `@Test(arguments:)`, which is evaluated synchronously, so
/// the toolchain queries that decide the list have to block.
private func computedSynchronously<T: Sendable>(name: String, _ body: @Sendable @escaping () async throws -> T) -> T {
  nonisolated(unsafe) var result: Result<T, any Error>!
  let sema = WrappedSemaphore(name: name)
  Task { @Sendable in
    do {
      result = .success(try await body())
    } catch {
      result = .failure(error)
    }
    sema.signal()
  }
  try! sema.waitOrThrow()
  return try! result.get()
}

let swiftPMHasExperimentalBuildServer: Bool = computedSynchronously(name: "swiftPMHasExperimentalBuildServer") {
  if ProcessEnv.block["SWIFTCI_USE_LOCAL_DEPS"] != nil {
    // In general, don't skip tests in CI. Toolchain should be up-to-date.
    return false
  }
  let swift = try await unwrap(ToolchainRegistry.forTesting.default?.swift).deletingLastPathComponent()
    .appending(component: "swift")
  let output = try await Process.run(
    arguments: [
      try swift.filePath,
      "package",
      "experimental-build-server",
      "--help",
    ],
    // "swift package experimental-build-server --help" times out without a working directory. Set a nonsensical
    // working directory to make it finish faster.
    workingDirectory: TSCBasic.AbsolutePath(validating: testScratchDir().filePath)
  )
  return try output.utf8stderrOutput().contains("Usage: swift package experimental-build-server")
}

/// The Swift version of the toolchain used for testing or `nil` if it could not be determined.
private let testingToolchainSwiftVersion: SwiftVersion? = computedSynchronously(
  name: "testingToolchainSwiftVersion"
) {
  try await ToolchainRegistry.forTesting.default?.swiftVersion
}

/// The build server configurations that a test should be run with.
///
/// The out-of-process build server is included if the toolchain used for testing has one. Tests that rely on build
/// server behavior which was added during the development of a Swift version pass that version as
/// `requiredSwiftVersion`, so that they only exercise the out-of-process build server with a toolchain that can have
/// that behavior.
private func buildServerOptionsToTest(
  requiring requiredSwiftVersion: SwiftVersion? = nil
) -> [SourceKitLSPOptions] {
  let native: [SourceKitLSPOptions] = [.forTestingNativeSwiftPMBuildServer]
  guard swiftPMHasExperimentalBuildServer else {
    return native
  }
  if let requiredSwiftVersion {
    guard let testingToolchainSwiftVersion, testingToolchainSwiftVersion >= requiredSwiftVersion else {
      return native
    }
  }
  return native + [.forTestingExperimentalSwiftPMBuildServer]
}

@Suite(.serialized, .configureLogging)
struct SwiftPMBuildServerTests {
  @Test
  func testNoPackage() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": ""
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let buildServerSpec = SwiftPMBuildServer.searchForConfig(in: packageRoot, options: try await .testDefault())
      #expect(buildServerSpec == nil)
    }
  }

  @Test
  func testNoToolchain() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      await expectThrowsError(
        try await SwiftPMBuildServer(
          projectRoot: packageRoot,
          toolchainRegistry: ToolchainRegistry(toolchains: []),
          options: SourceKitLSPOptions(),
          connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"),
          testHooks: SwiftPMTestHooks()
        )
      )
    }
  }

  @Test
  func testRelativeScratchPath() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let options = SourceKitLSPOptions(
        swiftPM: .init(
          scratchPath: "non_default_relative_build_path"
        ),
        backgroundIndexing: false
      )
      let swiftpmBuildServer = try await SwiftPMBuildServer(
        projectRoot: packageRoot,
        toolchainRegistry: .forTesting,
        options: options,
        connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"),
        testHooks: SwiftPMTestHooks()
      )

      let dataPath = await swiftpmBuildServer.destinationBuildParameters.dataPath
      let expectedScratchPath = packageRoot.appending(component: try #require(options.swiftPMOrDefault.scratchPath))
      #expect(dataPath.asURL.isDescendant(of: expectedScratchPath))
    }
  }

  @Test
  func testExtraArgumentsArePassedVerbatim() async throws {
    try await withTestScratchDir { tempDir in
      let extraArguments = [
        "--experimental-prebuilts-download-url",
        "file:swift-syntax-prebuilt/feed",
        "--experimental-prebuilts-root-cert",
        "swift-syntax-prebuilt/root.cer",
      ]
      let config = try await BuildServerConfig.forSwiftPMBuildServer(
        projectRoot: tempDir,
        options: SourceKitLSPOptions(
          swiftPM: .init(extraArguments: extraArguments)
        ),
        toolchainRegistry: .forTesting
      )

      #expect(Array(config.argv.suffix(extraArguments.count)) == extraArguments)
    }
  }

  @Test(arguments: buildServerOptionsToTest())
  func testBasicSwiftArgs(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:5.8
          import PackageDescription
          let package = Package(
            name: "a",
            platforms: [.macOS(.v13)],
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = try tempDir.appending(component: "pkg").realpath
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswift =
        packageRoot
        .appending(components: "Sources", "lib", "a.swift")
      let build = try await buildPath(root: packageRoot, platform: hostTriple.platformBuildPathComponent)

      _ = try #require(await buildServerManager.initializationData?.indexDatabasePath)
      _ = try #require(await buildServerManager.initializationData?.indexStorePath)
      let arguments = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments

      expectArgumentsContain("-module-name", "lib", arguments: arguments)
      expectArgumentsContain("-parse-as-library", arguments: arguments)

      expectArgumentsContain("-target", arguments: arguments)  // Only one!
      #if os(macOS)
      let versionString = "13.0"  // matches the explicit platforms: [.macOS(.v13)] above
      if options.swiftPMOrDefault.buildSystem == .swiftbuild {
        expectArgumentsContain(
          "-target",
          // Account for differences in macOS naming canonicalization
          try await hostTriple.tripleString(forPlatformVersion: versionString).replacing("macosx", with: "macos"),
          arguments: arguments
        )
      } else {
        expectArgumentsContain(
          "-target",
          try await hostTriple.tripleString(forPlatformVersion: versionString),
          arguments: arguments
        )
      }
      expectArgumentsContain(
        "-sdk",
        arguments: arguments,
        allowMultiple: options.swiftPMOrDefault.buildSystem == .swiftbuild
      )
      expectArgumentsContain("-F", arguments: arguments, allowMultiple: true)
      #else
      expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: arguments)
      #endif

      if options.swiftPMOrDefault.buildSystem != .swiftbuild {
        // Swift Build and the native build system setup search paths differently. We deliberately avoid testing implementation details of Swift Build here.
        expectArgumentsContain("-I", try build.appending(component: "Modules").filePath, arguments: arguments)
      }

      expectArgumentsContain(try aswift.filePath, arguments: arguments)
    }
  }

  @Test
  func testCompilerArgumentsForFileThatContainsPlusCharacterURLEncoded() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Sources/lib/a+something.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = try tempDir.appending(component: "pkg").realpath
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: SourceKitLSPOptions(),
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aPlusSomething =
        packageRoot
        .appending(components: "Sources", "lib", "a+something.swift")

      _ = try #require(await buildServerManager.initializationData?.indexStorePath)
      // Simulate an LSP client that percent-encodes `+` as `%2B` in file URIs.
      let urlWithPlusEscaped = try #require(
        URL(string: aPlusSomething.absoluteString.replacing("+", with: "%2B"))
      )
      let arguments = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(urlWithPlusEscaped),
          language: .swift,
          fallbackAfterTimeout: false
        )
      )
      .compilerArguments

      // Check that we have both source files in the compiler arguments, which means that we didn't compute the compiler
      // arguments for a+something.swift using substitute arguments from a.swift.
      #expect(
        try arguments.contains(aPlusSomething.filePath),
        "Compiler arguments do not contain a+something.swift: \(arguments)"
      )
      #expect(
        try arguments.contains(
          packageRoot.appending(components: "Sources", "lib", "a.swift")
            .filePath
        ),
        "Compiler arguments do not contain a.swift: \(arguments)"
      )
    }
  }

  @Test(arguments: buildServerOptionsToTest(requiring: SwiftVersion(6, 5)))
  func testBuildSetup(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")

      let swiftPMOptions = SourceKitLSPOptions.SwiftPMOptions(
        configuration: .release,
        scratchPath: try packageRoot.appending(component: "non_default_build_path").filePath,
        cCompilerFlags: ["-m32"],
        swiftCompilerFlags: ["-typecheck"],
        buildSystem: options.swiftPMOrDefault.buildSystem
      )

      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: SourceKitLSPOptions(swiftPM: swiftPMOptions),
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswift =
        packageRoot
        .appending(components: "Sources", "lib", "a.swift")

      let arguments = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments

      expectArgumentsContain("-typecheck", arguments: arguments)
      expectArgumentsContain("-Xcc", "-m32", arguments: arguments)
      expectArgumentsContain("-O", arguments: arguments)
    }
  }

  @Test
  func testManifestArgs() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: SourceKitLSPOptions(),
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let source = try packageRoot.appending(component: "Package.swift").realpath
      let arguments = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(source),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments

      expectArgumentsContain("-swift-version", "4.2", arguments: arguments)
      expectArgumentsContain(try source.filePath, arguments: arguments)
    }
  }

  @Test(arguments: buildServerOptionsToTest())
  func testMultiFileSwift(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Sources/lib/b.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = try tempDir.appending(component: "pkg").realpath
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswift =
        packageRoot
        .appending(components: "Sources", "lib", "a.swift")
      let bswift =
        packageRoot
        .appending(components: "Sources", "lib", "b.swift")

      let argumentsA = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments
      expectArgumentsContain(try aswift.filePath, arguments: argumentsA)
      expectArgumentsContain(try bswift.filePath, arguments: argumentsA)
      let argumentsB = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments
      expectArgumentsContain(try aswift.filePath, arguments: argumentsB)
      expectArgumentsContain(try bswift.filePath, arguments: argumentsB)
    }
  }

  @Test(arguments: buildServerOptionsToTest())
  func testMultiTargetSwift(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/libA/a.swift": "",
          "pkg/Sources/libB/b.swift": "",
          "pkg/Sources/libC/include/libC.h": "",
          "pkg/Sources/libC/libC.c": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [
              .target(name: "libA", dependencies: ["libB", "libC"]),
              .target(name: "libB"),
              .target(name: "libC"),
            ]
          )
          """,
        ]
      )
      let packageRoot = try tempDir.appending(component: "pkg").realpath
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswift =
        packageRoot
        .appending(components: "Sources", "libA", "a.swift")
      let bswift =
        packageRoot
        .appending(components: "Sources", "libB", "b.swift")
      let arguments = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments
      expectArgumentsContain(try aswift.filePath, arguments: arguments)
      expectArgumentsDoNotContain(try bswift.filePath, arguments: arguments)
      let libCInclude = try packageRoot.appending(components: "Sources", "libC", "include").filePath
      if options.swiftPMOrDefault.buildSystem == .swiftbuild {
        expectArgumentsContain("-I\(libCInclude)", arguments: arguments)
      } else {
        expectArgumentsContain("-Xcc", "-I", "-Xcc", libCInclude, arguments: arguments)
      }

      let argumentsB = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(bswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments
      expectArgumentsContain(try bswift.filePath, arguments: argumentsB)
      expectArgumentsDoNotContain(try aswift.filePath, arguments: argumentsB)
      #expect(
        !argumentsB.contains { $0.contains(libCInclude) },
      )
    }
  }

  @Test(arguments: buildServerOptionsToTest())
  func testUnknownFile(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/libA/a.swift": "",
          "pkg/Sources/libB/b.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "libA")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswift =
        packageRoot
        .appending(components: "Sources", "libA", "a.swift")
      let bswift =
        packageRoot
        .appending(components: "Sources", "libB", "b.swift")
      _ = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      )
      #expect(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(bswift),
          language: .swift,
          fallbackAfterTimeout: false
        )?.isFallback == true
      )
      #expect(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(URL(string: "https://www.apple.com")!),
          language: .swift,
          fallbackAfterTimeout: false
        )?.isFallback == true
      )
    }
  }

  @Test(arguments: buildServerOptionsToTest())
  func testBasicCXXArgs(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.cpp": "",
          "pkg/Sources/lib/b.cpp": "",
          "pkg/Sources/lib/include/a.h": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")],
            cxxLanguageStandard: .cxx14
          )
          """,
        ]
      )
      let packageRoot = try tempDir.appending(component: "pkg").realpath
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let acxx =
        packageRoot
        .appending(components: "Sources", "lib", "a.cpp")
      let bcxx =
        packageRoot
        .appending(components: "Sources", "lib", "b.cpp")
      let header =
        packageRoot
        .appending(components: "Sources", "lib", "include", "a.h")
      let build = buildPath(root: packageRoot, platform: try await hostTriple.platformBuildPathComponent)

      _ = try #require(await buildServerManager.initializationData?.indexStorePath)

      for file in [acxx, header] {
        let args = try #require(
          await buildServerManager.buildSettingsInferredFromMainFile(
            for: DocumentURI(file),
            language: .cpp,
            fallbackAfterTimeout: false
          )
        ).compilerArguments

        expectArgumentsContain("-std=c++14", arguments: args)

        expectArgumentsDoNotContain("-arch", arguments: args)
        expectArgumentsContain("-target", arguments: args)  // Only one!
        #if os(macOS)
        let versionString = PackageModel.Platform.macOS.oldestSupportedVersion.versionString
        if options.swiftPMOrDefault.buildSystem == .swiftbuild {
          expectArgumentsContain(
            "-target",
            // Account for differences in macOS naming canonicalization
            try await hostTriple.tripleString(forPlatformVersion: versionString).replacing("macosx", with: "macos"),
            arguments: args
          )
        } else {
          expectArgumentsContain(
            "-target",
            try await hostTriple.tripleString(forPlatformVersion: versionString),
            arguments: args
          )
        }
        expectArgumentsContain("-isysroot", arguments: args)
        if options.swiftPMOrDefault.buildSystem == .swiftbuild {
          // Swift Build passes framework search paths as combined `-F<path>` arguments.
          #expect(args.contains { $0.hasPrefix("-F") })
        } else {
          expectArgumentsContain("-F", arguments: args, allowMultiple: true)
        }
        #else
        expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: args)
        #endif

        let includePath =
          try packageRoot
          .appending(components: "Sources", "lib", "include")
          .filePath
        if options.swiftPMOrDefault.buildSystem == .swiftbuild {
          // Swift Build passes the include path as a single combined `-I<path>` argument.
          expectArgumentsContain("-I\(includePath)", arguments: args)
        } else {
          expectArgumentsContain("-I", includePath, arguments: args)
        }
        expectArgumentsDoNotContain("-I", try build.filePath, arguments: args)
        expectArgumentsDoNotContain(try bcxx.filePath, arguments: args)

        if options.swiftPMOrDefault.buildSystem == .swiftbuild {
          #expect(
            args.contains("-o") && args.contains { $0.hasSuffix(".o") },
            "Expected an object-file output in \(args)"
          )
        } else {
          URL(fileURLWithPath: try build.appending(components: "lib.build", "a.cpp.o").filePath)
            .withUnsafeFileSystemRepresentation {
              expectArgumentsContain("-o", String(cString: $0!), arguments: args)
            }
        }
      }
    }
  }

  @Test(arguments: buildServerOptionsToTest())
  func testDeploymentTargetSwift(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:5.7
          import PackageDescription
          let package = Package(name: "a",
            platforms: [.macOS(.v13)],
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswift =
        packageRoot
        .appending(components: "Sources", "lib", "a.swift")
      let arguments = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments
      expectArgumentsContain("-target", arguments: arguments)  // Only one!

      #if os(macOS)
      if options.swiftPMOrDefault.buildSystem == .swiftbuild {
        try await expectArgumentsContain(
          "-target",
          // Account for differences in macOS naming canonicalization
          hostTriple.tripleString(forPlatformVersion: "13.0").replacing("macosx", with: "macos"),
          arguments: arguments
        )
      } else {
        try await expectArgumentsContain(
          "-target",
          hostTriple.tripleString(forPlatformVersion: "13.0"),
          arguments: arguments
        )
      }
      #else
      expectArgumentsContain("-target", try await hostTriple.tripleString, arguments: arguments)
      #endif
    }
  }

  @Test(arguments: buildServerOptionsToTest(requiring: SwiftVersion(6, 5)))
  func testSymlinkInWorkspaceSwift(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg_real/Sources/lib/a.swift": "",
          "pkg_real/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")

      try FileManager.default.createSymbolicLink(
        at: URL(fileURLWithPath: packageRoot.filePath),
        withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").filePath)
      )

      let buildServerSpec = try #require(
        SwiftPMBuildServer.searchForConfig(in: packageRoot, options: await .testDefault())
      )
      let buildServerManager = await BuildServerManager(
        buildServerSpec: buildServerSpec,
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswiftSymlink =
        packageRoot
        .appending(components: "Sources", "lib", "a.swift")
      let aswiftReal = try aswiftSymlink.realpath
      let manifest = packageRoot.appending(component: "Package.swift")

      let argumentsFromSymlink = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswiftSymlink),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments

      // We opened the project from a symlink. The realpath isn't part of the project and we should thus not receive
      // build settings for it.
      #expect(
        try #require(
          await buildServerManager.buildSettingsInferredFromMainFile(
            for: DocumentURI(aswiftReal),
            language: .swift,
            fallbackAfterTimeout: false
          )
        ).isFallback
      )
      expectArgumentsContain(try aswiftSymlink.filePath, arguments: argumentsFromSymlink)
      expectArgumentsDoNotContain(try aswiftReal.filePath, arguments: argumentsFromSymlink)

      let argsManifest = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(manifest),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments

      expectArgumentsContain(try manifest.filePath, arguments: argsManifest)
      expectArgumentsDoNotContain(try manifest.realpath.filePath, arguments: argsManifest)
    }
  }

  @Test(arguments: buildServerOptionsToTest(requiring: SwiftVersion(6, 5)))
  func testSymlinkInWorkspaceCXX(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg_real/Sources/lib/a.cpp": "",
          "pkg_real/Sources/lib/b.cpp": "",
          "pkg_real/Sources/lib/include/a.h": "",
          "pkg_real/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")],
            cxxLanguageStandard: .cxx14
          )
          """,
        ]
      )

      let acpp = ["Sources", "lib", "a.cpp"]
      let ah = ["Sources", "lib", "include", "a.h"]

      let realRoot = tempDir.appending(component: "pkg_real")
      let symlinkRoot = tempDir.appending(component: "pkg")

      try FileManager.default.createSymbolicLink(
        at: URL(fileURLWithPath: symlinkRoot.filePath),
        withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").filePath)
      )

      let buildServerSpec = try #require(
        SwiftPMBuildServer.searchForConfig(in: symlinkRoot, options: await .testDefault())
      )
      let buildServerManager = await BuildServerManager(
        buildServerSpec: buildServerSpec,
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      for file in [acpp, ah] {
        let args = try #require(
          await buildServerManager.buildSettingsInferredFromMainFile(
            for: DocumentURI(symlinkRoot.appending(components: file)),
            language: .cpp,
            fallbackAfterTimeout: false
          )?
          .compilerArguments
        )
        expectArgumentsDoNotContain(try realRoot.appending(components: file).filePath, arguments: args)
        expectArgumentsContain(try symlinkRoot.appending(components: file).filePath, arguments: args)
      }
    }
  }

  @Test(arguments: buildServerOptionsToTest())
  func testSwiftDerivedSources(options: SourceKitLSPOptions) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Sources/lib/a.txt": "",
          "pkg/Package.swift": """
          // swift-tools-version:5.3
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib", resources: [.copy("a.txt")])]
          )
          """,
        ]
      )
      let packageRoot = try tempDir.appending(component: "pkg").realpath
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: options,
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswift =
        packageRoot
        .appending(components: "Sources", "lib", "a.swift")
      let arguments = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      )
      .compilerArguments
      expectArgumentsContain(try aswift.filePath, arguments: arguments)
      _ = try #require(
        arguments.firstIndex(where: {
          $0.hasSuffix(".swift") && $0.contains("DerivedSources")
        }),
        "missing resource_bundle_accessor.swift from \(arguments)"
      )
    }
  }

  @Test
  func testNestedInvalidPackageSwift() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/Package.swift": "// not a valid package",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let workspaceRoot =
        tempDir
        .appending(components: "pkg", "Sources", "lib")

      let buildServerSpec = SwiftPMBuildServer.searchForConfig(in: workspaceRoot, options: try await .testDefault())
      #expect(buildServerSpec == nil)
    }
  }

  @Test
  func testPluginArgs() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Plugins/MyPlugin/a.swift": "",
          "pkg/Sources/lib/lib.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:5.7
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [
              .target(name: "lib"),
              .plugin(name: "MyPlugin", capability: .buildTool)
            ]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: SourceKitLSPOptions(),
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()

      let aswift =
        packageRoot
        .appending(components: "Plugins", "MyPlugin", "a.swift")

      _ = try #require(await buildServerManager.initializationData?.indexStorePath)
      let arguments = try #require(
        await buildServerManager.buildSettingsInferredFromMainFile(
          for: DocumentURI(aswift),
          language: .swift,
          fallbackAfterTimeout: false
        )
      ).compilerArguments

      // Plugins get compiled with the same compiler arguments as the package manifest
      expectArgumentsContain("-package-description-version", "5.7.0", arguments: arguments)
      expectArgumentsContain(try aswift.filePath, arguments: arguments)
    }
  }

  @Test
  func testPackageWithDependencyWithoutResolving() async throws {
    // This package has a dependency but we haven't run `swift package resolve`. We don't want to resolve packages from
    // SourceKit-LSP because it has side-effects to the build directory.
    // But even without the dependency checked out, we should be able to create a SwiftPMBuildServer and retrieve the
    // existing source files.
    let project = try await SwiftPMTestProject(
      files: [
        "Tests/PackageTests/PackageTests.swift": """
        import Testing

        1️⃣@Test func topLevelTestPassing() {}2️⃣
        """
      ],
      manifest: """
        let package = Package(
          name: "MyLibrary",
          dependencies: [.package(url: "https://github.com/swiftlang/swift-testing.git", branch: "main")],
          targets: [
            .testTarget(name: "PackageTests", dependencies: [.product(name: "Testing", package: "swift-testing")]),
          ]
        )
        """
    )

    let tests = try await project.testClient.send(WorkspaceTestsRequest())
    #expect(
      tests == [
        TestItem(
          id: "PackageTests.topLevelTestPassing()",
          label: "topLevelTestPassing()",
          disabled: false,
          style: "swift-testing",
          location: try project.location(from: "1️⃣", to: "2️⃣", in: "PackageTests.swift"),
          children: [],
          tags: []
        )
      ]
    )
  }

  @Test
  func testPackageLoadingWorkDoneProgress() async throws {
    let didReceiveWorkDoneProgressNotification = WrappedSemaphore(name: "work done progress received")
    let project = try await SwiftPMTestProject(
      files: [
        "MyLibrary/Test.swift": ""
      ],
      capabilities: ClientCapabilities(window: WindowClientCapabilities(workDoneProgress: true)),
      hooks: Hooks(
        buildServerHooks: BuildServerHooks(
          swiftPMTestHooks: SwiftPMTestHooks(reloadPackageDidStart: {
            didReceiveWorkDoneProgressNotification.waitOrXCTFail()
          })
        )
      ),
      pollIndex: false,
      preInitialization: { testClient in
        testClient.handleMultipleRequests { (request: CreateWorkDoneProgressRequest) in
          return VoidResponse()
        }
      }
    )
    let begin = try await project.testClient.nextNotification(ofType: WorkDoneProgress.self)
    #expect(begin.value == .begin(WorkDoneProgressBegin(title: "SourceKit-LSP: Reloading Package")))
    didReceiveWorkDoneProgressNotification.signal()

    let end = try await project.testClient.nextNotification(ofType: WorkDoneProgress.self)
    #expect(end.token == begin.token)
    #expect(end.value == .end(WorkDoneProgressEnd()))
  }

  @Test
  func testBuildSettingsForVersionSpecificPackageManifest() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
          "pkg/Package@swift-5.8.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = try tempDir.appending(component: "pkg").realpath
      let versionSpecificManifestURL = packageRoot.appending(component: "Package@swift-5.8.swift")
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: SourceKitLSPOptions(),
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()
      let settings = await buildServerManager.buildSettingsInferredFromMainFile(
        for: DocumentURI(versionSpecificManifestURL),
        language: .swift,
        fallbackAfterTimeout: false
      )
      let compilerArgs = try #require(settings?.compilerArguments)
      #expect(compilerArgs.contains("-package-description-version"))
      #expect(compilerArgs.contains(try versionSpecificManifestURL.filePath))
    }
  }

  @Test
  func testBuildSettingsForInvalidManifest() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:5.1
          import PackageDescription
          """,
        ]
      )
      let packageRoot = try tempDir.appending(component: "pkg").realpath
      let manifestURL = packageRoot.appending(component: "Package.swift")
      let buildServerManager = await BuildServerManager(
        buildServerSpec: .swiftpmSpec(for: packageRoot),
        toolchainRegistry: .forTesting,
        options: SourceKitLSPOptions(),
        connectionToClient: DummyBuildServerManagerConnectionToClient(),
        buildServerHooks: BuildServerHooks(),
        createMainFilesProvider: { _, _ in nil }
      )
      await buildServerManager.waitForUpToDateBuildGraph()
      let settings = await buildServerManager.buildSettingsInferredFromMainFile(
        for: DocumentURI(manifestURL),
        language: .swift,
        fallbackAfterTimeout: false
      )
      let compilerArgs = try #require(settings?.compilerArguments)
      expectArgumentsContain("-package-description-version", "5.1.0", arguments: compilerArgs)
      #expect(compilerArgs.contains(try manifestURL.filePath))
    }
  }

  @Test(
    .enabled(if: Platform.current != .windows, "Toolsets are not working on Windows, see swift-package-manager#9438.")
  )
  func testToolsets() async throws {
    let project = try await SwiftPMTestProject(
      files: [
        "Foo/foo.swift": """
        import Bar

        func foo() {
          bar()
        }
        """,
        "Bar/bar.swift": """
        #if BAR
        public func bar() {}
        #endif
        """,
        "/toolset.json": """
        {
          "schemaVersion": "1.0",
          "swiftCompiler": {
            "extraCLIOptions": [
              "-DBAR"
            ]
          }
        }
        """,
        "/.sourcekit-lsp/config.json": """
        {
          "swiftPM": {
            "toolsets": ["toolset.json"]
          }
        }
        """,
      ],
      manifest: """
        let package = Package(
          name: "MyLibrary",
          platforms: [.macOS(.v13)],
          targets: [
            .target(name: "Foo", dependencies: ["Bar"]),
            .target(name: "Bar"),
          ]
        )
        """,
      options: .testDefault(experimentalFeatures: [.sourceKitOptionsRequest]),
      enableBackgroundIndexing: true,
    )

    let (uri, _) = try project.openDocument("foo.swift")

    let options = try await project.testClient.send(
      SourceKitOptionsRequest(
        textDocument: TextDocumentIdentifier(uri),
        prepareTarget: false,
        allowFallbackSettings: false
      )
    )
    #expect(options.compilerArguments.contains("-DBAR"))

    let diagnostics = try #require(
      await project.testClient.send(
        DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
      ).fullReport?.items
    )
    #expect(diagnostics.isEmpty)
  }

  @Test(arguments: buildServerOptionsToTest())
  func testPkgConfigDirectories(options: SourceKitLSPOptions) async throws {
    var options = options
    options.experimentalFeatures = [.sourceKitOptionsRequest]
    options.swiftPMOrDefault.pkgConfigPaths = ["pcfiles"]
    let project = try await SwiftPMTestProject(
      files: [
        "MyExecutable/main.swift": """
        import MyClib
        """,
        "MyClib/module.modulemap": """
        module MyClib [system] {
          header "shim.h"
          link "myclib"
          export *
        }
        """,
        "MyClib/shim.h": "",
        "/pcfiles/myclib.pc": """
        Name: myclib
        Cflags: -I/sourcekit-lsp-test-pkg-config-include
        """,
      ],
      manifest: """
        let package = Package(
          name: "MyPkg",
          targets: [
            .executableTarget(name: "MyExecutable", dependencies: ["MyClib"]),
            .systemLibrary(name: "MyClib", pkgConfig: "myclib"),
          ]
        )
        """,
      options: options
    )

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

    let buildSettings = try await project.testClient.send(
      SourceKitOptionsRequest(
        textDocument: TextDocumentIdentifier(uri),
        prepareTarget: false,
        allowFallbackSettings: false
      )
    )
    #expect(buildSettings.compilerArguments.contains("-I/sourcekit-lsp-test-pkg-config-include"))
  }

  // MARK: - Package reload filtering

  /// Creates a minimal package containing a zip-based binary target and returns the server ready
  /// for reload-filtering tests.
  ///
  /// Using a zip file is important: SwiftPM extracts zipped binary targets into the scratch
  /// directory (`.build/index-build/artifacts/` by default), which makes those extracted paths
  /// part of `buildDescription.inputs`. That is exactly the condition under which file-change
  /// events in `.build/` would trigger a spurious package reload without the
  /// `isInScratchDirectory` fix.
  ///
  /// - Returns: A tuple of the running server and the project root URL.
  private func makeServerWithBinaryTargetAndWaitForInitialLoad(
    in tempDir: URL,
    options: SourceKitLSPOptions = SourceKitLSPOptions(),
    reloadPackageDidStart: (@Sendable () async -> Void)? = nil
  ) async throws -> (server: SwiftPMBuildServer, projectRoot: URL) {
    let artifactBundleName = "MyBinaryTool.artifactbundle"
    let zipName = "\(artifactBundleName).zip"

    try FileManager.default.createFiles(
      root: tempDir,
      files: [
        // Artifact bundle is staged outside of pkg/ so it doesn't pollute the package directory.
        // ZipArchiver.compress will zip it into pkg/ below.
        "\(artifactBundleName)/info.json": """
        {
          "schemaVersion": "1.0",
          "artifacts": {
            "MyBinaryTool": {
              "type": "executable",
              "version": "1.0.0",
              "variants": []
            }
          }
        }
        """,
        "pkg/Sources/lib/a.swift": "",
        "pkg/Package.swift": """
        // swift-tools-version:5.5
        import PackageDescription
        let package = Package(
          name: "pkg",
          targets: [
            .target(name: "lib"),
            .binaryTarget(name: "MyBinaryTool", path: "\(zipName)")
          ]
        )
        """,
      ]
    )

    let pkgDir = tempDir.appending(component: "pkg")
    try await UniversalArchiver(localFileSystem).compress(
      directory: Basics.AbsolutePath(validating: tempDir.appending(component: artifactBundleName).filePath),
      to: Basics.AbsolutePath(validating: pkgDir.appending(component: zipName).filePath)
    )
    try FileManager.default.removeItem(at: tempDir.appending(component: artifactBundleName))

    let projectRoot = try pkgDir.realpath
    let server = try await SwiftPMBuildServer(
      projectRoot: projectRoot,
      toolchainRegistry: .forTesting,
      options: options,
      connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"),
      testHooks: SwiftPMTestHooks(reloadPackageDidStart: reloadPackageDidStart)
    )
    _ = await server.waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest())
    return (server, projectRoot)
  }

  /// Verifies that file-change events for paths inside the SwiftPM scratch directory do not
  /// trigger a package reload.
  ///
  /// When a package contains a zip-based binary target, SwiftPM extracts the artifact into
  /// `<scratch>/artifacts/<pkg>/<target>/` on every package load.  Before the
  /// `isInScratchDirectory` fix those extracted paths were registered in
  /// `buildDescription.inputs`, so the resulting file-created/-deleted events passed
  /// `fileAffectsSwiftOrClangBuildSettings`, triggering another reload — an infinite loop.
  @Test
  func testBinaryTargetArtifactEventsDoNotTriggerPackageReload() async throws {
    try await withTestScratchDir { tempDir in
      let packageInitialized = ThreadSafeBox<Bool>(initialValue: false)
      let unexpectedReloadStarted = ThreadSafeBox<Bool>(initialValue: false)

      let (server, projectRoot) = try await makeServerWithBinaryTargetAndWaitForInitialLoad(
        in: tempDir,
        reloadPackageDidStart: {
          if packageInitialized.value {
            unexpectedReloadStarted.withLock { $0 = true }
          }
        }
      )
      packageInitialized.withLock { $0 = true }

      // SwiftPM extracts the artifact bundle to:
      //   <scratch>/artifacts/<package-identity>/<target-name>/<artifact-name>/
      // With the default options, scratch = .build/index-build/.
      let extractedInfoJSON = projectRoot.appending(
        components: ".build",
        "index-build",
        "artifacts",
        "pkg",
        "MyBinaryTool",
        "MyBinaryTool.artifactbundle",
        "info.json"
      )
      #expect(FileManager.default.fileExists(atPath: extractedInfoJSON.path))
      await server.didChangeWatchedFiles(
        notification: OnWatchedFilesDidChangeNotification(
          changes: [
            FileEvent(uri: DocumentURI(extractedInfoJSON), type: .deleted),
            FileEvent(uri: DocumentURI(extractedInfoJSON), type: .created),
          ]
        )
      )

      _ = await server.waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest())
      #expect(!unexpectedReloadStarted.value)
    }
  }

  /// Same scenario with a custom scratch path configured outside of `.build/`.
  ///
  /// When a custom scratch path is used, SourceKit-LSP's artifact extraction goes to
  /// `<custom-scratch>/artifacts/`, which the first `isInScratchDirectory` check covers.
  /// The second check (default `.build/` directory) additionally suppresses events from
  /// whatever the regular `swift build` command writes to `.build/`.
  @Test
  func testBinaryTargetArtifactEventsDoNotTriggerPackageReloadWithCustomScratchPath()
    async throws
  {
    try await withTestScratchDir { tempDir in
      let customScratch = tempDir.appending(component: "custom-scratch")

      let packageInitialized = ThreadSafeBox<Bool>(initialValue: false)
      let unexpectedReloadStarted = ThreadSafeBox<Bool>(initialValue: false)

      let (server, projectRoot) = try await makeServerWithBinaryTargetAndWaitForInitialLoad(
        in: tempDir,
        options: SourceKitLSPOptions(swiftPM: .init(scratchPath: try customScratch.filePath)),
        reloadPackageDidStart: {
          if packageInitialized.value {
            unexpectedReloadStarted.withLock { $0 = true }
          }
        }
      )
      packageInitialized.withLock { $0 = true }

      // With a custom scratch path, SwiftPM extracts to <custom-scratch>/artifacts/.
      // Simulate the delete-and-re-expand cycle for those paths.
      let extractedInfoJSON = customScratch.appending(
        components: "artifacts",
        "pkg",
        "MyBinaryTool",
        "MyBinaryTool.artifactbundle",
        "info.json"
      )
      #expect(FileManager.default.fileExists(atPath: extractedInfoJSON.path))
      await server.didChangeWatchedFiles(
        notification: OnWatchedFilesDidChangeNotification(
          changes: [
            FileEvent(uri: DocumentURI(extractedInfoJSON), type: .deleted),
            FileEvent(uri: DocumentURI(extractedInfoJSON), type: .created),
          ]
        )
      )

      // Also verify that the default .build/ directory is filtered even when a custom
      // scratch path is configured (second isInScratchDirectory check).
      let defaultBuildArtifact = projectRoot.appending(
        components: ".build",
        "artifacts",
        "pkg",
        "MyBinaryTool",
        "MyBinaryTool.artifactbundle",
        "info.json"
      )
      await server.didChangeWatchedFiles(
        notification: OnWatchedFilesDidChangeNotification(
          changes: [FileEvent(uri: DocumentURI(defaultBuildArtifact), type: .created)]
        )
      )

      _ = await server.waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest())
      #expect(!unexpectedReloadStarted.value)
    }
  }

  /// Verifies that the build system is correctly inferred as 'native' from the `.buildSystem_debug` file.
  @Test(
    arguments: [SwiftPMBuildSystem.native, .swiftbuild],
    [SKOptions.BuildConfiguration.debug, .release]
  )
  func testBuildSystemInferenceFromFile(
    buildSystem: SwiftPMBuildSystem,
    buildConfiguration: SKOptions.BuildConfiguration,
  ) async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
          "pkg/.build/.buildSystem_\(buildConfiguration)": "\(buildSystem)",
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let options = SourceKitLSPOptions(swiftPM: .init(configuration: buildConfiguration))

      let spec = try #require(
        SwiftPMBuildServer.searchForConfig(in: packageRoot, options: options)
      )
      if case .swiftPM(let inferredBuildSystem) = spec.kind {
        #expect(
          inferredBuildSystem == buildSystem,
          "Expected \(buildSystem) but got \(String(describing: inferredBuildSystem))"
        )
      } else {
        Issue.record("Expected swiftPM build server kind")
      }
    }
  }

  /// Verifies that the build system inference falls back to heuristics when the file doesn't exist.
  @Test
  func testBuildSystemInferenceFallbackToHeuristics() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let buildDir = packageRoot.appending(component: ".build")
      try FileManager.default.createDirectory(at: buildDir, withIntermediateDirectories: true)

      // Create 'debug' directory to trigger native heuristic
      let debugDir = buildDir.appending(component: "debug")
      try FileManager.default.createDirectory(at: debugDir, withIntermediateDirectories: true)

      let spec = try #require(
        SwiftPMBuildServer.searchForConfig(in: packageRoot, options: SourceKitLSPOptions())
      )
      if case .swiftPM(let inferredBuildSystem) = spec.kind {
        #expect(
          inferredBuildSystem == .native,
          "Expected .native from heuristic but got \(String(describing: inferredBuildSystem))"
        )
      } else {
        Issue.record("Expected swiftPM build server kind")
      }
    }
  }

  /// Verifies that when both build system outputs exist, swiftbuild is preferred.
  @Test
  func testBuildSystemInferencePreferSwiftBuildWhenBothExist() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")
      let buildDir = packageRoot.appending(component: ".build")
      try FileManager.default.createDirectory(at: buildDir, withIntermediateDirectories: true)

      // Create both 'debug' and 'out' directories
      let debugDir = buildDir.appending(component: "debug")
      let outDir = buildDir.appending(component: "out")
      try FileManager.default.createDirectory(at: debugDir, withIntermediateDirectories: true)
      try FileManager.default.createDirectory(at: outDir, withIntermediateDirectories: true)

      let spec = try #require(
        SwiftPMBuildServer.searchForConfig(in: packageRoot, options: SourceKitLSPOptions())
      )
      if case .swiftPM(let inferredBuildSystem) = spec.kind {
        #expect(
          inferredBuildSystem == .swiftbuild,
          "Expected .swiftbuild when both outputs exist but got \(String(describing: inferredBuildSystem))"
        )
      } else {
        Issue.record("Expected swiftPM build server kind")
      }
    }
  }

  /// Verifies that the correct `.buildSystem_{config}` file is read based on the configuration when both exist.
  @Test
  func testBuildSystemInferenceUsesCorrectConfigurationFile() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
          "pkg/.build/.buildSystem_debug": "native",
          "pkg/.build/.buildSystem_release": "swiftbuild",
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")

      // Test with debug configuration (default)
      let debugSpec = try #require(
        SwiftPMBuildServer.searchForConfig(in: packageRoot, options: SourceKitLSPOptions())
      )
      if case .swiftPM(let inferredBuildSystem) = debugSpec.kind {
        #expect(
          inferredBuildSystem == .native,
          "Expected .native for debug config but got \(String(describing: inferredBuildSystem))"
        )
      } else {
        Issue.record("Expected swiftPM build server kind for debug")
      }

      // Test with release configuration
      let releaseOptions = SourceKitLSPOptions(swiftPM: .init(configuration: .release))
      let releaseSpec = try #require(
        SwiftPMBuildServer.searchForConfig(in: packageRoot, options: releaseOptions)
      )
      if case .swiftPM(let inferredBuildSystem) = releaseSpec.kind {
        #expect(
          inferredBuildSystem == .swiftbuild,
          "Expected .swiftbuild for release config but got \(String(describing: inferredBuildSystem))"
        )
      } else {
        Issue.record("Expected swiftPM build server kind for release")
      }
    }
  }

  /// Verifies that invalid content in the `.buildSystem_{config}` file results in nil inference.
  @Test
  func testBuildSystemInferenceInvalidContent() async throws {
    try await withTestScratchDir { tempDir in
      try FileManager.default.createFiles(
        root: tempDir,
        files: [
          "pkg/Sources/lib/a.swift": "",
          "pkg/Package.swift": """
          // swift-tools-version:4.2
          import PackageDescription
          let package = Package(
            name: "a",
            targets: [.target(name: "lib")]
          )
          """,
          "pkg/.build/.buildSystem_debug": "invalid_build_system",
        ]
      )
      let packageRoot = tempDir.appending(component: "pkg")

      let spec = try #require(
        SwiftPMBuildServer.searchForConfig(in: packageRoot, options: SourceKitLSPOptions())
      )
      if case .swiftPM(let inferredBuildSystem) = spec.kind {
        #expect(
          inferredBuildSystem == nil,
          "Expected nil for invalid build system but got \(String(describing: inferredBuildSystem))"
        )
      } else {
        Issue.record("Expected swiftPM build server kind")
      }
    }
  }

  @Test
  func testPackagePlugin() async throws {
    let testProject = try await SwiftPMTestProject(
      files: [
        "Test.swift": """
        #if NonDefaultTrait
        #warning("Trait enabled")
        #endif
        """
      ],
      manifest: """
        // swift-tools-version: 6.2

        import PackageDescription

        let package = Package(
          name: "MyLibrary",
          traits: [
            .default(enabledTraits: []),
            "NonDefaultTrait",
          ],
          targets: [.target(name: "MyLibrary")]
        )
        """,
      options: SourceKitLSPOptions(swiftPM: .init(traits: ["NonDefaultTrait"])),
      enableBackgroundIndexing: true
    )

    let (uri, _) = try testProject.openDocument("Test.swift")
    let diagnostics = try await testProject.testClient.send(
      DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
    )

    #expect(diagnostics.fullReport?.items.map(\.message) == ["Trait enabled"])
  }
}

private func expectArgumentsDoNotContain(
  _ pattern: String...,
  arguments: [String],
  sourceLocation: SourceLocation = #_sourceLocation
) {
  if let index = arguments.firstRange(of: pattern)?.startIndex {
    Issue.record(
      "not-pattern \(pattern) unexpectedly found at \(index) in arguments \(arguments)",
      sourceLocation: sourceLocation
    )
    return
  }
}

private func expectArgumentsContain(
  _ pattern: String...,
  arguments: [String],
  allowMultiple: Bool = false,
  sourceLocation: SourceLocation = #_sourceLocation
) {
  guard let index = arguments.firstRange(of: pattern)?.startIndex else {
    Issue.record("pattern \(pattern) not found in arguments \(arguments)", sourceLocation: sourceLocation)
    return
  }

  if !allowMultiple, let index2 = arguments[(index + 1)...].firstRange(of: pattern)?.startIndex {
    Issue.record(
      "pattern \(pattern) found twice (\(index), \(index2)) in \(arguments)",
      sourceLocation: sourceLocation
    )
  }
}

private func buildPath(
  root: URL,
  options: SourceKitLSPOptions.SwiftPMOptions = SourceKitLSPOptions.SwiftPMOptions(),
  platform: String
) -> URL {
  let buildPath =
    if let scratchPath = options.scratchPath {
      URL(fileURLWithPath: scratchPath)
    } else {
      root.appending(components: ".build", "index-build")
    }
  return buildPath.appending(components: platform, "\(options.configuration ?? .debug)")
}

fileprivate extension URL {
  func appending(components: [String]) -> URL {
    var result = self
    for component in components {
      result.appendPathComponent(component)
    }
    return result
  }
}

fileprivate extension BuildServerSpec {
  static func swiftpmSpec(for packageRoot: URL) -> BuildServerSpec {
    return BuildServerSpec(
      kind: .swiftPM(inferredBuildSystem: .native),
      projectRoot: packageRoot,
      configPath: packageRoot.appending(component: "Package.swift")
    )
  }
}
#endif
