import Foundation
import SourceKittenFramework
import TestHelpers
import XCTest

@testable import SwiftLintFramework

// swiftlint:disable file_length

private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is any OptInRule }).map(\.0)

final class ConfigurationTests: SwiftLintTestCase { // swiftlint:disable:this type_body_length
    // MARK: Setup & Teardown
    private var previousWorkingDir: String! // swiftlint:disable:this implicitly_unwrapped_optional

    override func setUp() {
        super.setUp()
        Configuration.resetCache()
        previousWorkingDir = FileManager.default.currentDirectoryPath
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
    }

    override func tearDown() {
        super.tearDown()
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(previousWorkingDir))
    }

    // MARK: Tests
    func testInit() {
        XCTAssertNotNil(
            try? Configuration(dict: [:]),
            "initializing Configuration with empty Dictionary should succeed"
        )
        XCTAssertNotNil(
            try? Configuration(dict: ["a": 1, "b": 2]),
            "initializing Configuration with valid Dictionary should succeed"
        )
    }

    func testNoConfiguration() {
        // Change to a folder where there is no `.swiftlint.yml`
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.emptyFolder.filepath))

        // Test whether the default configuration is used if there is no `.swiftlint.yml` or other config file
        XCTAssertEqual(Configuration(configurationFiles: []), Configuration.default)
    }

    func testEmptyConfiguration() {
        guard let config = try? Configuration(dict: [:]) else {
            XCTFail("empty YAML string should yield non-nil Configuration")
            return
        }
        XCTAssertEqual(config.rulesWrapper.disabledRuleIdentifiers, [])
        XCTAssertEqual(config.includedPaths, [])
        XCTAssertEqual(config.excludedPaths, [])
        XCTAssertEqual(config.indentation, .spaces(count: 4))
        XCTAssertEqual(config.reporter, "xcode")
        XCTAssertEqual(reporterFrom(identifier: config.reporter).identifier, "xcode")
        XCTAssertFalse(config.allowZeroLintableFiles)
        XCTAssertFalse(config.strict)
        XCTAssertFalse(config.lenient)
        XCTAssertNil(config.baseline)
        XCTAssertNil(config.writeBaseline)
        XCTAssertFalse(config.checkForUpdates)
    }

    func testInitWithRelativePathAndRootPath() {
        let expectedConfig = Mock.Config._0

        let config = Configuration(configurationFiles: [".swiftlint.yml".url()])

        XCTAssertEqual(config.rulesWrapper.disabledRuleIdentifiers, expectedConfig.rulesWrapper.disabledRuleIdentifiers)
        XCTAssertEqual(config.includedPaths, expectedConfig.includedPaths)
        XCTAssertEqual(config.excludedPaths, expectedConfig.excludedPaths)
        XCTAssertEqual(config.indentation, expectedConfig.indentation)
        XCTAssertEqual(config.reporter, expectedConfig.reporter)
        XCTAssertTrue(config.allowZeroLintableFiles)
        XCTAssertTrue(config.strict)
        XCTAssertEqual(config.baseline, expectedConfig.baseline)
        XCTAssertEqual(config.writeBaseline, expectedConfig.writeBaseline)
    }

    func testEnableAllRulesConfiguration() throws {
        let configuration = try Configuration(
            dict: [:],
            enableAllRules: true,
            cachePath: nil
        )

        XCTAssertEqual(configuration.rules.count, RuleRegistry.shared.list.list.count)
    }

    func testOnlyRule() throws {
        let configuration = try Configuration(
            dict: [:],
            onlyRule: ["nesting"],
            cachePath: nil
        )

        XCTAssertEqual(configuration.rules.count, 1)
    }

    func testOnlyRuleMultiple() throws {
        let onlyRuleIdentifiers = ["nesting", "todo"].sorted()
        let configuration = try Configuration(
            dict: ["only_rules": "line_length"],
            onlyRule: onlyRuleIdentifiers,
            cachePath: nil
        )
        XCTAssertEqual(onlyRuleIdentifiers, configuration.enabledRuleIdentifiers)

        let childConfiguration = try Configuration(dict: ["disabled_rules": onlyRuleIdentifiers.last ?? ""])
        let mergedConfiguration = configuration.merged(withChild: childConfiguration)
        XCTAssertEqual(onlyRuleIdentifiers.dropLast(), mergedConfiguration.enabledRuleIdentifiers)
    }

    func testOnlyRules() throws {
        let only = ["nesting", "todo"]

        let config = try Configuration(dict: ["only_rules": only])
        let configuredIdentifiers = config.rules.map {
            type(of: $0).identifier
        }.sorted()
        XCTAssertEqual(only, configuredIdentifiers)
    }

    func testOnlyRulesWithCustomRules() throws {
        // All custom rules from a config file should be active if the `custom_rules` is included in the `only_rules`
        // As the behavior is different for custom rules from parent configs, this test is helpful
        let only = ["custom_rules"]
        let customRuleIdentifier = "my_custom_rule"
        let customRules = [customRuleIdentifier: ["name": "A name for this custom rule", "regex": "this is illegal"]]

        let config = try Configuration(dict: ["only_rules": only, "custom_rules": customRules])
        guard let resultingCustomRules = config.rules.customRules else {
            XCTFail("Custom rules are expected to be present")
            return
        }
        XCTAssertTrue(
            resultingCustomRules.configuration.customRuleConfigurations.contains {
                $0.identifier == customRuleIdentifier
            }
        )
    }

    func testOnlyRulesWithSpecificCustomRules() throws {
        // Individual custom rules can be specified on the command line without specifying `custom_rules` as well.
        let customRuleIdentifier = "my_custom_rule"
        let customRuleIdentifier2 = "my_custom_rule2"
        let only = ["custom_rules"]
        let customRules = [
            customRuleIdentifier: ["name": "A custom rule", "regex": "this is illegal"],
            customRuleIdentifier2: ["name": "Another custom rule", "regex": "this is also illegal"],
        ]

        let configuration = try Configuration(
            dict: [
                "only_rules": only,
                "custom_rules": customRules,
            ],
            onlyRule: [customRuleIdentifier]
        )
        let resultingCustomRules = configuration.rules.customRules
        XCTAssertNotNil(resultingCustomRules)

        let enabledCustomRuleIdentifiers =
            resultingCustomRules?.configuration.customRuleConfigurations.map(\.identifier)
        XCTAssertEqual(enabledCustomRuleIdentifiers, [customRuleIdentifier])
    }

    func testWarningThreshold_value() throws {
        let config = try Configuration(dict: ["warning_threshold": 5])
        XCTAssertEqual(config.warningThreshold, 5)
    }

    func testWarningThreshold_nil() throws {
        let config = try Configuration(dict: [:])
        XCTAssertNil(config.warningThreshold)
    }

    func testOtherRuleConfigurationsAlongsideOnlyRules() {
        let only = ["nesting", "todo"]
        let enabledRulesConfigDict = [
            "opt_in_rules": ["line_length"],
            "only_rules": only,
        ]
        let disabledRulesConfigDict = [
            "disabled_rules": ["identifier_name"],
            "only_rules": only,
        ]
        let combinedRulesConfigDict = enabledRulesConfigDict.reduce(into: disabledRulesConfigDict) { $0[$1.0] = $1.1 }
        var configuration = try? Configuration(dict: enabledRulesConfigDict)
        XCTAssertNil(configuration)
        configuration = try? Configuration(dict: disabledRulesConfigDict)
        XCTAssertNil(configuration)
        configuration = try? Configuration(dict: combinedRulesConfigDict)
        XCTAssertNil(configuration)
    }

    func testDisabledRules() throws {
        let disabledConfig = try Configuration(dict: ["disabled_rules": ["nesting", "todo"]])
        XCTAssertEqual(disabledConfig.rulesWrapper.disabledRuleIdentifiers,
                       ["nesting", "todo"],
                       "initializing Configuration with valid rules in Dictionary should succeed")
        let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys
            .filter({ !(["nesting", "todo"] + optInRules).contains($0) }))
        let configuredIdentifiers = Set(disabledConfig.rules.map {
            type(of: $0).identifier
        })
        XCTAssertEqual(expectedIdentifiers, configuredIdentifiers)
    }

    func testDisabledRulesWithUnknownRule() throws {
        let validRule = "nesting"
        let bogusRule = "no_sprites_with_elf_shoes"

        let configuration = try Configuration(dict: ["disabled_rules": [validRule, bogusRule]])

        XCTAssertEqual(configuration.rulesWrapper.disabledRuleIdentifiers,
                       [validRule],
                       "initializing Configuration with valid rules in YAML string should succeed")
        let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys
            .filter({ !([validRule] + optInRules).contains($0) }))
        XCTAssertEqual(expectedIdentifiers, Set(configuration.enabledRuleIdentifiers))
    }

    func testDuplicatedRules() {
        let duplicateConfig1 = try? Configuration(dict: ["only_rules": ["todo", "todo"]])
        XCTAssertEqual(
            duplicateConfig1?.rules.count, 1, "duplicate rules should be removed when initializing Configuration"
        )

        let duplicateConfig2 = try? Configuration(dict: ["opt_in_rules": [optInRules.first!, optInRules.first!]])
        XCTAssertEqual(
            duplicateConfig2?.rules.filter { type(of: $0).identifier == optInRules.first! }.count, 1,
            "duplicate rules should be removed when initializing Configuration"
        )

        let duplicateConfig3 = try? Configuration(dict: ["disabled_rules": ["todo", "todo"]])
        XCTAssertEqual(
            duplicateConfig3?.rulesWrapper.disabledRuleIdentifiers.count, 1,
            "duplicate rules should be removed when initializing Configuration"
        )
    }

    func testIncludedExcludedRelativeLocationLevel1() {
        guard !isRunningWithBazel else {
            return
        }

        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level1.filepath))

        // The included path "File.swift" should be put relative to the configuration file
        // (~> Resources/ProjectMock/File.swift) and not relative to the path where
        // SwiftLint is run from (~> Resources/ProjectMock/Level1/File.swift)
        let configuration = Configuration(configurationFiles: ["../custom_included_excluded.yml".url()])
        let actualIncludedPath = configuration.includedPaths.first!
        let desiredIncludedPath = "File1.swift".url(relativeTo: Mock.Dir.level0)
        let actualExcludedPath = configuration.excludedPaths.first!
        let desiredExcludedPath = "File2.swift".url(relativeTo: Mock.Dir.level0)

        XCTAssertEqual(actualIncludedPath.path, desiredIncludedPath.path)
        XCTAssertEqual(actualExcludedPath.path, desiredExcludedPath.path)
    }

    func testIncludedExcludedRelativeLocationLevel0() {
        // Same as testIncludedPathRelatedToConfigurationFileLocationLevel1(),
        // but run from the directory the config file resides in
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(configurationFiles: ["custom_included_excluded.yml".url()])
        let actualIncludedPath = configuration.includedPaths.first!
        let desiredIncludedPath = "File1.swift".url(relativeTo: Mock.Dir.level0)
        let actualExcludedPath = configuration.excludedPaths.first!
        let desiredExcludedPath = "File2.swift".url(relativeTo: Mock.Dir.level0)

        XCTAssertEqual(actualIncludedPath.path, desiredIncludedPath.path)
        XCTAssertEqual(actualExcludedPath.path, desiredExcludedPath.path)
    }

    func testExcludedPaths() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath))
        let configuration = Configuration(
            includedPaths: ["directory".url()],
            excludedPaths: ["directory/excluded".url(), "directory/ExcludedFile.swift".url()]
        )

        let paths = configuration.lintablePaths(
            inPath: URL.cwd,
            forceExclude: false,
            excludeByPrefix: false
        )

        assertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
    }

    func testForceExcludesFile() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath))
        let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift".url()])

        let paths = configuration.lintablePaths(
            inPath: "directory/ExcludedFile.swift".url(),
            forceExclude: true,
            excludeByPrefix: false
        )

        XCTAssert(paths.isEmpty)
    }

    func testForceExcludesFileNotPresentInExcluded() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath))
        let configuration = Configuration(
            includedPaths: ["directory".url()],
            excludedPaths: ["directory/ExcludedFile.swift".url(), "directory/excluded".url()]
        )

        let paths = configuration.lintablePaths(
            inPath: URL.cwd,
            forceExclude: true,
            excludeByPrefix: false
        )

        assertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
    }

    func testForceExcludesDirectory() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath))
        let configuration = Configuration(excludedPaths: ["directory/excluded".url()])

        let paths = configuration.lintablePaths(
            inPath: "directory".url(),
            forceExclude: true,
            excludeByPrefix: false
        )

        assertEqual(["directory/File1.swift", "directory/File2.swift", "directory/ExcludedFile.swift"], paths)
    }

    func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath))
        let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift".url()])

        let paths = configuration.lintablePaths(
            inPath: "directory".url(),
            forceExclude: true,
            excludeByPrefix: false
        )

        assertEqual(["directory/File1.swift", "directory/File2.swift", "directory/excluded/Excluded.swift"], paths)
    }

    func testLintablePaths() {
        let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0,
                                                        forceExclude: false,
                                                        excludeByPrefix: false)
        let filenames = paths.map(\.lastPathComponent).sorted()
        let expectedFilenames = [
            "DirectoryLevel1.swift",
            "Level0.swift", "Level1.swift", "Level2.swift", "Level3.swift",
            "Main.swift", "Sub.swift",
        ]

        XCTAssertEqual(Set(expectedFilenames), Set(filenames))
    }

    func testGlobIncludePaths() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(includedPaths: ["**/Level2".url()])
        let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
                                                forceExclude: true,
                                                excludeByPrefix: false)
        let filenames = paths.map(\.lastPathComponent).sorted()
        let expectedFilenames = ["Level2.swift", "Level3.swift"]

        XCTAssertEqual(Set(expectedFilenames), Set(filenames))
    }

    func testDuplicatedGlobIncludePaths() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(includedPaths: ["**/Level2".url(), "**/Level2".url()])
        let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
                                                forceExclude: true,
                                                excludeByPrefix: false)
        let filenames = paths.map(\.lastPathComponent).sorted()
        let expectedFilenames = ["Level2.swift", "Level3.swift"]

        XCTAssertEqual(expectedFilenames, filenames)
    }

    func testGlobExcludePaths() {
        let configuration = Configuration(
            includedPaths: [Mock.Dir.level3],
            excludedPaths: [Mock.Dir.level3.appending(path: "*.swift")]
        )

        let lintablePaths = configuration.lintablePaths(inPath: URL.cwd,
                                                        forceExclude: false,
                                                        excludeByPrefix: false)
        XCTAssertEqual(lintablePaths, [])
    }

    // MARK: - Testing Configuration Equality

    func testIsEqualTo() {
        XCTAssertEqual(Mock.Config._0, Mock.Config._0)
    }

    func testIsNotEqualTo() {
        XCTAssertNotEqual(Mock.Config._0, Mock.Config._2)
    }

    // MARK: - Testing Custom Configuration File

    func testCustomConfiguration() {
        let file = SwiftLintFile(path: Mock.Swift._0)!
        XCTAssertNotEqual(Mock.Config._0.configuration(for: file),
                          Mock.Config._0Custom.configuration(for: file))
    }

    func testConfigurationWithSwiftFileAsRoot() {
        let configuration = Configuration(configurationFiles: [Mock.Yml._0])

        let file = SwiftLintFile(path: Mock.Swift._0)!
        XCTAssertEqual(configuration.configuration(for: file), configuration)
    }

    func testConfigurationWithSwiftFileAsRootAndCustomConfiguration() {
        let configuration = Mock.Config._0Custom

        let file = SwiftLintFile(path: Mock.Swift._0)!
        XCTAssertEqual(configuration.configuration(for: file), configuration)
    }

    // MARK: - Testing custom indentation

    func testIndentationTabs() throws {
        let configuration = try Configuration(dict: ["indentation": "tabs"])
        XCTAssertEqual(configuration.indentation, .tabs)
    }

    func testIndentationSpaces() throws {
        let configuration = try Configuration(dict: ["indentation": 2])
        XCTAssertEqual(configuration.indentation, .spaces(count: 2))
    }

    func testIndentationFallback() throws {
        let configuration = try Configuration(dict: ["indentation": "invalid"])
        XCTAssertEqual(configuration.indentation, .spaces(count: 4))
    }

    // MARK: - Testing Rules from config dictionary

    private let testRuleList = RuleList(rules: RuleWithLevelsMock.self)

    func testConfiguresCorrectlyFromDict() throws {
        let ruleConfiguration = [1, 2]
        let config = [RuleWithLevelsMock.identifier: ruleConfiguration]
        let rules = try testRuleList.allRulesWrapped(configurationDict: config).map(\.rule)
        // swiftlint:disable:next xct_specific_matcher
        XCTAssertTrue(rules == [try RuleWithLevelsMock(configuration: ruleConfiguration)])
    }

    func testConfigureFallsBackCorrectly() throws {
        let config = [RuleWithLevelsMock.identifier: ["a", "b"]]
        let rules = try testRuleList.allRulesWrapped(configurationDict: config).map(\.rule)
        // swiftlint:disable:next xct_specific_matcher
        XCTAssertTrue(rules == [RuleWithLevelsMock()])
    }

    func testAllowZeroLintableFiles() throws {
        let configuration = try Configuration(dict: ["allow_zero_lintable_files": true])
        XCTAssertTrue(configuration.allowZeroLintableFiles)
    }

    func testStrict() throws {
        let configuration = try Configuration(dict: ["strict": true])
        XCTAssertTrue(configuration.strict)
    }

    func testLenient() throws {
        let configuration = try Configuration(dict: ["lenient": true])
        XCTAssertTrue(configuration.lenient)
    }

    func testBaseline() throws {
        let baselinePath = "Baseline.json"
        let configuration = try Configuration(dict: ["baseline": baselinePath])
        XCTAssertEqual(configuration.baseline, baselinePath.url())
    }

    func testWriteBaseline() throws {
        let baselinePath = "Baseline.json"
        let configuration = try Configuration(dict: ["write_baseline": baselinePath])
        XCTAssertEqual(configuration.writeBaseline, baselinePath.url())
    }

    func testCheckForUpdates() throws {
        let configuration = try Configuration(dict: ["check_for_updates": true])
        XCTAssertTrue(configuration.checkForUpdates)
    }

    // MARK: - ExcludeByPrefix option tests

    func testExcludeByPrefixExcludedPaths() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(
            includedPaths: ["Level1".url()],
            excludedPaths: ["Level1/Level1.swift".url(), "Level1/Level2/Level3".url()]
        )
        let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
                                                forceExclude: false,
                                                excludeByPrefix: true)
        let filenames = paths.map(\.lastPathComponent)
        XCTAssertEqual(filenames, ["Level2.swift"])
    }

    func testExcludeByPrefixForceExcludesFile() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift".url()])
        let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift".url(),
                                                forceExclude: true,
                                                excludeByPrefix: true)
        XCTAssertEqual([], paths)
    }

    func testExcludeByPrefixForceExcludesFileNotPresentInExcluded() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(includedPaths: ["Level1".url()],
                                          excludedPaths: ["Level1/Level1.swift".url()])
        let paths = configuration.lintablePaths(inPath: "Level1".url(),
                                                forceExclude: true,
                                                excludeByPrefix: true)
        let filenames = paths.map(\.lastPathComponent).sorted()
        XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames)
    }

    func testExcludeByPrefixForceExcludesDirectory() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(
            excludedPaths: [
                "Level1/Level2".url(),
                "Directory.swift".url(),
                "ChildConfig".url(),
                "ParentConfig".url(),
                "NestedConfig".url(),
            ]
        )
        let paths = configuration.lintablePaths(inPath: URL.cwd,
                                                forceExclude: true,
                                                excludeByPrefix: true)
        let filenames = paths.map(\.lastPathComponent).sorted()
        XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames)
    }

    func testExcludeByPrefixForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(
            excludedPaths: [
                "Level1".url(),
                "Directory.swift/DirectoryLevel1.swift".url(),
                "ChildConfig".url(),
                "ParentConfig".url(),
                "NestedConfig".url(),
            ]
        )
        let paths = configuration.lintablePaths(inPath: URL.cwd,
                                                forceExclude: true,
                                                excludeByPrefix: true)
        let filenames = paths.map(\.lastPathComponent)
        XCTAssertEqual(["Level0.swift"], filenames)
    }

    func testExcludeByPrefixGlobExcludePaths() {
        XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath))
        let configuration = Configuration(
            includedPaths: ["Level1".url()],
            excludedPaths: ["Level1/*/*.swift".url(), "Level1/*/*/*.swift".url()]
        )
        let paths = configuration.lintablePaths(inPath: "Level1".url(),
                                                forceExclude: false,
                                                excludeByPrefix: true)
        let filenames = paths.map(\.lastPathComponent).sorted()
        XCTAssertEqual(filenames, ["Level1.swift"])
    }

    func testDictInitWithCachePath() throws {
        let configuration = try Configuration(
            dict: ["cache_path": "cache/path/1"]
        )

        XCTAssertEqual(configuration.cachePath, "cache/path/1")
    }

    func testDictInitWithCachePathFromCommandLine() throws {
        let configuration = try Configuration(
            dict: ["cache_path": "cache/path/1"],
            cachePath: "cache/path/2"
        )

        XCTAssertEqual(configuration.cachePath, "cache/path/2")
    }

    func testMainInitWithCachePath() {
        let configuration = Configuration(
            configurationFiles: [],
            cachePath: "cache/path/1"
        )

        XCTAssertEqual(configuration.cachePath, "cache/path/1")
    }

    // This test demonstrates an existing bug: when the Configuration is obtained from the in-memory cache, the
    // cachePath is not taken into account
    //
    // This issue may not be reproducible under normal execution: the cache is in memory, so when a user changes
    // the cachePath from command line and re-runs swiftlint, cache is not reused leading to the correct behavior
    func testMainInitWithCachePathAndCachedConfig() {
        let configuration1 = Configuration(
            configurationFiles: [],
            cachePath: "cache/path/1"
        )

        let configuration2 = Configuration(
            configurationFiles: [],
            cachePath: "cache/path/2"
        )

        XCTAssertEqual(configuration1.cachePath, "cache/path/1")
        XCTAssertEqual(configuration2.cachePath, "cache/path/1")
    }

    private func assertEqual(_ relativeExpectedPaths: [String],
                             _ actualPaths: [URL],
                             file: StaticString = #filePath,
                             line: UInt = #line) {
        XCTAssertEqual(
            relativeExpectedPaths.absolutePathsStandardized().sorted(),
            actualPaths.map(\.path).absolutePathsStandardized().sorted(),
            file: file,
            line: line
        )
    }
}

private extension Sequence where Element == String {
    func absolutePathsStandardized() -> [String] {
        // In Bazel builds, absolute paths might be prefixed with `/private`.
        map { URL(filePath: $0).standardizedFileURL.resolvingSymlinksInPath().relativeDisplayPath }
    }
}

private extension Configuration {
    var enabledRuleIdentifiers: [String] {
        rules.map {
            type(of: $0).identifier
        }.sorted()
    }
}
