//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import SourceKitLSP
import LanguageServerProtocol
import SKCore
import SKSupport
import IndexStoreDB
import ISDBTibs
import ISDBTestSupport
import XCTest
import Foundation
import LSPTestSupport

import enum PackageLoading.Platform
import struct TSCBasic.AbsolutePath
import struct PackageModel.BuildFlags

public typealias URL = Foundation.URL

public final class SKTibsTestWorkspace {

  public let tibsWorkspace: TibsTestWorkspace
  public let testServer: TestSourceKitServer

  public var index: IndexStoreDB { tibsWorkspace.index }
  public var builder: TibsBuilder { tibsWorkspace.builder }
  public var sources: TestSources { tibsWorkspace.sources }
  public var sk: TestClient { testServer.client }

  public init(
    immutableProjectDir: URL,
    persistentBuildDir: URL,
    tmpDir: URL,
    removeTmpDir: Bool,
    toolchain: Toolchain,
    clientCapabilities: ClientCapabilities,
    testServer: TestSourceKitServer? = nil
  ) throws
  {
    self.testServer = testServer ?? TestSourceKitServer(connectionKind: .local)
    self.tibsWorkspace = try TibsTestWorkspace(
      immutableProjectDir: immutableProjectDir,
      persistentBuildDir: persistentBuildDir,
      tmpDir: tmpDir,
      removeTmpDir: removeTmpDir,
      toolchain: TibsToolchain(toolchain))

    try initWorkspace(clientCapabilities: clientCapabilities)
  }

  public init(
    projectDir: URL,
    tmpDir: URL,
    toolchain: Toolchain,
    clientCapabilities: ClientCapabilities,
    testServer: TestSourceKitServer? = nil
  ) throws {
    self.testServer = testServer ?? TestSourceKitServer(connectionKind: .local)

    self.tibsWorkspace = try TibsTestWorkspace(
      projectDir: projectDir,
      tmpDir: tmpDir,
      toolchain: TibsToolchain(toolchain))

    try initWorkspace(clientCapabilities: clientCapabilities)
  }

  func initWorkspace(clientCapabilities: ClientCapabilities) throws {
    let buildPath = try AbsolutePath(validating: builder.buildRoot.path)
    let buildSystem = CompilationDatabaseBuildSystem(projectRoot: buildPath)
    let indexDelegate = SourceKitIndexDelegate()
    tibsWorkspace.delegate = indexDelegate

    let workspace = Workspace(
      documentManager: DocumentManager(),
      rootUri: DocumentURI(sources.rootDirectory),
      capabilityRegistry: CapabilityRegistry(clientCapabilities: clientCapabilities),
      toolchainRegistry: ToolchainRegistry.shared,
      buildSetup: BuildSetup(configuration: .debug, path: buildPath, flags: BuildFlags()),
      underlyingBuildSystem: buildSystem,
      index: index,
      indexDelegate: indexDelegate)

    workspace.buildSystemManager.delegate = testServer.server!
    testServer.server!._workspaces = [workspace]
  }
}

extension SKTibsTestWorkspace {

  public func testLoc(_ name: String) -> TestLocation { sources.locations[name]! }

  public func buildAndIndex() throws {
    try tibsWorkspace.buildAndIndex()
  }

  /// Perform a group of edits to the project sources and optionally rebuild.
  public func edit(
    rebuild: Bool = false,
    _ block: (inout TestSources.ChangeBuilder, _ current: SourceFileCache) throws -> ()) throws
  {
    try tibsWorkspace.edit(rebuild: rebuild, block)
  }
}

extension SKTibsTestWorkspace {
  public func openDocument(_ url: URL, language: Language) throws {
    sk.send(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
      uri: DocumentURI(url),
      language: language,
      version: 1,
      text: try sources.sourceCache.get(url))))
  }
}

extension XCTestCase {

  public func staticSourceKitTibsWorkspace(
    name: String,
    clientCapabilities: ClientCapabilities = .init(),
    tmpDir: URL? = nil,
    removeTmpDir: Bool = true,
    server: TestSourceKitServer? = nil
  ) throws -> SKTibsTestWorkspace? {
    let testDirName = testDirectoryName
    let workspace = try SKTibsTestWorkspace(
      immutableProjectDir: XCTestCase.sklspInputsDirectory
        .appendingPathComponent(name, isDirectory: true),
      persistentBuildDir: XCTestCase.productsDirectory
        .appendingPathComponent("sk-tests/\(testDirName)", isDirectory: true),
      tmpDir: tmpDir ?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
        .appendingPathComponent("sk-test-data/\(testDirName)", isDirectory: true),
      removeTmpDir: removeTmpDir,
      toolchain: ToolchainRegistry.shared.default!,
      clientCapabilities: clientCapabilities,
      testServer: server
    )

    if workspace.builder.targets.contains(where: { target in !target.clangTUs.isEmpty })
      && !workspace.builder.toolchain.clangHasIndexSupport {
      fputs("warning: skipping test because '\(workspace.builder.toolchain.clang.path)' does not " +
            "have indexstore support; use swift-clang\n", stderr)
      return nil
    }

    return workspace
  }

  /// Constructs a mutable SKTibsTestWorkspace for the given test from INPUTS.
  ///
  /// The resulting workspace allow edits and is not persistent.
  public func mutableSourceKitTibsTestWorkspace(
    name: String,
    clientCapabilities: ClientCapabilities = .init(),
    tmpDir: URL? = nil
  ) throws -> SKTibsTestWorkspace? {
    let testDirName = testDirectoryName
    let workspace = try SKTibsTestWorkspace(
      projectDir: XCTestCase.sklspInputsDirectory.appendingPathComponent(name, isDirectory: true),
      tmpDir: tmpDir ?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
        .appendingPathComponent("sk-test-data/\(testDirName)", isDirectory: true),
      toolchain: ToolchainRegistry.shared.default!,
      clientCapabilities: clientCapabilities)

    if workspace.builder.targets.contains(where: { target in !target.clangTUs.isEmpty })
      && !workspace.builder.toolchain.clangHasIndexSupport {
      fputs("warning: skipping test because '\(workspace.builder.toolchain.clang.path)' does not " +
            "have indexstore support; use swift-clang\n", stderr)
      return nil
    }

    return workspace
  }

  /// The path to the INPUTS directory of shared test projects.
  public static var sklspInputsDirectory: URL = {
    #if os(macOS)
    // FIXME: Use Bundle.module.resourceURL once the fix for SR-12912 is released.
    var resources = XCTestCase.productsDirectory
      .appendingPathComponent("SourceKitLSP_SKTestSupport.bundle")
      .appendingPathComponent("Contents")
      .appendingPathComponent("Resources")
    if !FileManager.default.fileExists(atPath: resources.path) {
      // Xcode and command-line swiftpm differ about the path.
      resources.deleteLastPathComponent()
      resources.deleteLastPathComponent()
    }
    #else
    let resources = XCTestCase.productsDirectory
      .appendingPathComponent("SourceKitLSP_SKTestSupport.resources")
    #endif
    guard FileManager.default.fileExists(atPath: resources.path) else {
      fatalError("missing resources \(resources.path)")
    }
    return resources.appendingPathComponent("INPUTS", isDirectory: true).standardizedFileURL
  }()
}

extension TestLocation {
  public var position: Position {
    Position(self)
  }
}

extension Position {
  public init(_ loc: TestLocation) {
    self.init(line: loc.line - 1, utf16index: loc.utf16Column - 1)
  }

  /// Incorrectly use the UTF-8 column index in place of the UTF-16 one, to match the incorrect
  /// implementation in SourceKitServer when using the index.
  public init(badUTF16 loc: TestLocation) {
    self.init(line: loc.line - 1, utf16index: loc.utf8Column - 1)
  }
}

extension Location {
  public init(_ loc: TestLocation) {
    self.init(uri: DocumentURI(loc.url), range: Range(Position(loc)))
  }

  /// Incorrectly use the UTF-8 column index in place of the UTF-16 one, to match the incorrect
  /// implementation in SourceKitServer when using the index.
  public init(badUTF16 loc: TestLocation) {
    self.init(uri: DocumentURI(loc.url), range: Range(Position(badUTF16: loc)))
  }
}

extension TibsToolchain {
  public convenience init(_ sktc: Toolchain) {
    let execExt: String = Platform.current?.executableExtension ?? ""

    let ninja: URL?
    if let ninjaPath = ProcessInfo.processInfo.environment["NINJA_BIN"] {
      ninja = URL(fileURLWithPath: ninjaPath, isDirectory: false)
    } else {
      ninja = findTool(name: "ninja\(execExt)")
    }
    self.init(
      swiftc: sktc.swiftc!.asURL,
      clang: sktc.clang!.asURL,
      libIndexStore: sktc.libIndexStore!.asURL,
      tibs: XCTestCase.productsDirectory.appendingPathComponent("tibs\(execExt)", isDirectory: false),
      ninja: ninja)
  }
}

extension TestLocation {
  public var docIdentifier: TextDocumentIdentifier { TextDocumentIdentifier(url) }
  public var docUri: DocumentURI { DocumentURI(url) }
}
