import Foundation
import TestHelpers
import Testing

@testable import SwiftLintBuiltInRules
@testable import SwiftLintFramework

// swiftlint:disable file_length

private extension Configuration {
    func contains<T: Rule>(rule _: T.Type) -> Bool {
        rules.contains { $0 is T }
    }
}

@Suite(.rulesRegistered)
struct MultipleConfigurationsTests { // swiftlint:disable:this type_body_length
    // MARK: - Rules Merging
    @Test(.workingDirectory(Constants.Dir.level0))
    func merge() {
        let config0Merge2 = Constants.Config._0.merged(withChild: Constants.Config._2)

        #expect(!Constants.Config._0.contains(rule: ForceCastRule.self))
        #expect(Constants.Config._2.contains(rule: ForceCastRule.self))
        #expect(!config0Merge2.contains(rule: ForceCastRule.self))

        #expect(Constants.Config._0.contains(rule: TodoRule.self))
        #expect(Constants.Config._2.contains(rule: TodoRule.self))
        #expect(config0Merge2.contains(rule: TodoRule.self))

        #expect(!Constants.Config._3.contains(rule: TodoRule.self))
        #expect(!config0Merge2.merged(withChild: Constants.Config._3).contains(rule: TodoRule.self))
    }

    // MARK: - Merging Aspects
    @Test
    func warningThresholdMerging() {
        func configuration(forWarningThreshold warningThreshold: Int?) -> Configuration {
            Configuration(
                warningThreshold: warningThreshold,
                reporter: XcodeReporter.identifier
            )
        }
        #expect(configuration(forWarningThreshold: 3)
            .merged(withChild: configuration(forWarningThreshold: 2)).warningThreshold == 2)
        #expect(configuration(forWarningThreshold: nil)
            .merged(withChild: configuration(forWarningThreshold: 2)).warningThreshold == 2)
        #expect(configuration(forWarningThreshold: 3)
            .merged(withChild: configuration(forWarningThreshold: nil)).warningThreshold == 3)
        #expect(configuration(forWarningThreshold: nil)
            .merged(withChild: configuration(forWarningThreshold: nil)).warningThreshold == nil)
    }

    @Test
    func onlyRulesMerging() {
        let baseConfiguration = Configuration(
            rulesMode: .defaultConfiguration(
                disabled: [],
                optIn: [
                    ForceTryRule.identifier,
                    ForceCastRule.identifier,
                ]
            )
        )
        let onlyConfiguration = Configuration(rulesMode: .onlyConfiguration([TodoRule.identifier]))
        #expect(baseConfiguration.contains(rule: TodoRule.self))
        #expect(onlyConfiguration.rules.count == 1)
        #expect(onlyConfiguration.rules.first is TodoRule)

        let mergedConfiguration1 = baseConfiguration.merged(withChild: onlyConfiguration)
        #expect(mergedConfiguration1.rules.count == 1)
        #expect(mergedConfiguration1.rules.first is TodoRule)

        // Also test the other way around
        let mergedConfiguration2 = onlyConfiguration.merged(withChild: baseConfiguration)
        #expect(mergedConfiguration2.rules.count == 3) // 2 opt-ins + 1 from the only rules
        #expect(mergedConfiguration2.contains(rule: TodoRule.self))
        #expect(mergedConfiguration2.contains(rule: ForceCastRule.self))
        #expect(mergedConfiguration2.contains(rule: ForceTryRule.self))
    }

    @Test
    func onlyRuleMerging() {
        let ruleIdentifier = TodoRule.identifier
        let onlyRuleConfiguration = Configuration.onlyRuleConfiguration(ruleIdentifier)

        let emptyDefaultConfiguration = Configuration.emptyDefaultConfiguration()
        let mergedConfiguration1 = onlyRuleConfiguration.merged(withChild: emptyDefaultConfiguration)
        #expect(mergedConfiguration1.rules.count == 1)
        #expect(mergedConfiguration1.rules.first is TodoRule)

        let disabledDefaultConfiguration = Configuration.disabledDefaultConfiguration(ruleIdentifier)
        let mergedConfiguration2 = onlyRuleConfiguration.merged(withChild: disabledDefaultConfiguration)
        #expect(mergedConfiguration2.rules.isEmpty)

        let enabledOnlyConfiguration = Configuration.enabledOnlyConfiguration(ForceTryRule.identifier)
        let mergedConfiguration3 = onlyRuleConfiguration.merged(withChild: enabledOnlyConfiguration)
        #expect(mergedConfiguration3.rules.count == 1)
        #expect(mergedConfiguration3.rules.first is TodoRule)
    }

    @Test
    func customRulesMerging() {
        let mergedConfiguration = Constants.Config._0CustomRules.merged(
            withChild: Constants.Config._2CustomRules,
            rootDirectory: URL.cwd
        )
        guard let mergedCustomRules = mergedConfiguration.rules.customRules
            else {
            Issue.record("Custom rules are expected to be present")
            return
        }
        #expect(
            mergedCustomRules.configuration.customRuleConfigurations.contains { $0.identifier == "no_abc" }
        )
        #expect(
            mergedCustomRules.configuration.customRuleConfigurations.contains { $0.identifier == "no_abcd" }
        )
    }

    @Test
    func mergingAllowsDisablingParentsCustomRules() {
        let mergedConfiguration = Constants.Config._0CustomRules.merged(
            withChild: Constants.Config._2CustomRulesDisabled,
            rootDirectory: URL.cwd
        )
        guard let mergedCustomRules = mergedConfiguration.rules.customRules
            else {
            Issue.record("Custom rules are expected to be present")
            return
        }
        #expect(!mergedCustomRules.configuration.customRuleConfigurations.contains { $0.identifier == "no_abc" })
        #expect(mergedCustomRules.configuration.customRuleConfigurations.contains { $0.identifier == "no_abcd" })
    }

    @Test
    func customRulesMergingWithOnlyRulesCase1() {
        // The base configuration is in only rules mode
        // The child configuration is in the default rules mode
        // => all custom rules should be considered
        let mergedConfiguration = Constants.Config._0CustomRulesOnly.merged(
            withChild: Constants.Config._2CustomRules,
            rootDirectory: URL.cwd
        )
        guard let mergedCustomRules = mergedConfiguration.rules.customRules
            else {
            Issue.record("Custom rules are expected to be present")
            return
        }
        #expect(
            mergedCustomRules.configuration.customRuleConfigurations.contains { $0.identifier == "no_abc" }
        )
        #expect(
            mergedCustomRules.configuration.customRuleConfigurations.contains { $0.identifier == "no_abcd" }
        )
    }

    @Test
    func customRulesMergingWithOnlyRulesCase2() {
        // The base configuration is in only rules mode
        // The child configuration is in the only rules mode
        // => only the custom rules from the child configuration should be considered
        // (because custom rules from base configuration would require explicit mention as one of the `only_rules`)
        let mergedConfiguration = Constants.Config._0CustomRulesOnly.merged(
            withChild: Constants.Config._2CustomRulesOnly,
            rootDirectory: URL.cwd
        )
        guard let mergedCustomRules = mergedConfiguration.rules.customRules
            else {
            Issue.record("Custom rules are expected to be present")
            return
        }
        #expect(!mergedCustomRules.configuration.customRuleConfigurations.contains { $0.identifier == "no_abc" })
        #expect(mergedCustomRules.configuration.customRuleConfigurations.contains { $0.identifier == "no_abcd" })
    }

    @Test
    func customRulesReconfiguration() {
        // Custom Rule severity gets reconfigured to "error"
        let mergedConfiguration = Constants.Config._0CustomRulesOnly.merged(
            withChild: Constants.Config._2CustomRulesReconfig,
            rootDirectory: URL.cwd
        )
        guard let mergedCustomRules = mergedConfiguration.rules.customRules
            else {
            Issue.record("Custom rules are expected to be present")
            return
        }
        #expect(
            mergedCustomRules.configuration.customRuleConfigurations.filter { $0.identifier == "no_abc" }.count == 1
        )
        guard let customRule = (mergedCustomRules.configuration.customRuleConfigurations.first {
            $0.identifier == "no_abc"
        }) else {
            Issue.record("Custom rule is expected to be present")
            return
        }
        #expect(customRule.severityConfiguration.severity == .error)
    }

    // MARK: - Nested Configurations
    @Test(.workingDirectory(Constants.Dir.level0))
    func level0() {
        #expect(
            Constants.Config._0.configuration(for: SwiftLintFile(path: Constants.Swift._0)!)
                == Constants.Config._0
        )
    }

    @Test(.workingDirectory(Constants.Dir.level0))
    func level1() {
        #expect(
            Constants.Config._0.configuration(for: SwiftLintFile(path: Constants.Swift._1)!)
                == Constants.Config._0
        )
    }

    @Test
    func level2() {
        let config = Constants.Config._0.configuration(for: SwiftLintFile(path: Constants.Swift._2)!)
        var config2 = Constants.Config._2
        config2.fileGraph = Configuration.FileGraph(rootDirectory: Constants.Dir.level2)

        #expect(config == Constants.Config._0.merged(withChild: config2, rootDirectory: config.rootDirectory))
    }

    @Test
    func level3() {
        let config = Constants.Config._0.configuration(for: SwiftLintFile(path: Constants.Swift._3)!)
        var config3 = Constants.Config._3
        config3.fileGraph = Configuration.FileGraph(rootDirectory: Constants.Dir.level3)

        #expect(config == Constants.Config._0.merged(withChild: config3, rootDirectory: config.rootDirectory))
    }

    @Test
    func nestedConfigurationForOnePathPassedIn() {
        // If a path to one or more configuration files is specified, nested configurations should be ignored
        let config = Configuration(configurationFiles: [Constants.Yml._0])
        #expect(config.configuration(for: SwiftLintFile(path: Constants.Swift._3)!) == config)
    }

    @Test
    func parentConfigIsIgnoredAsNestedConfiguration() {
        // If a configuration has already been used to build the main config,
        // it should not again be regarded as a nested config
        #expect(
            Constants.Config.nested.configuration(for: SwiftLintFile(path: Constants.Swift.nestedSub)!)
                == Constants.Config.nested
        )
    }

    // MARK: - Child & Parent Configs
    @Test(
        .disabled(if: isRunningWithBazel),
        arguments: [Constants.Dir.childConfigTest1, Constants.Dir.childConfigTest2],
    )
    func validChildConfig(_ path: URL) {
        CurrentWorkingDirectory.$url.withValue(path) {
            assertEqualExceptForFileGraph(
                Configuration(configurationFiles: ["main.yml".url()]),
                Configuration(configurationFiles: ["expected.yml".url()])
            )
        }
    }

    @Test(arguments: [Constants.Dir.parentConfigTest1, Constants.Dir.parentConfigTest2])
    func validParentConfig(_ path: URL) {
        CurrentWorkingDirectory.$url.withValue(path) {
            assertEqualExceptForFileGraph(
                Configuration(configurationFiles: ["main.yml".url()]),
                Configuration(configurationFiles: ["expected.yml".url()])
            )
        }
    }

    @Test(
        .disabled(if: isRunningWithBazel),
        arguments: [Constants.Dir.childConfigTest1, Constants.Dir.childConfigTest2],
    )
    func commandLineChildConfigs(_ path: URL) {
        CurrentWorkingDirectory.$url.withValue(path) {
            assertEqualExceptForFileGraph(
                Configuration(
                    configurationFiles: [
                        "main.yml".url(),
                        "child1.yml".url(),
                        "child2.yml".url(),
                    ]
                ),
                Configuration(configurationFiles: ["expected.yml".url()])
            )
        }
    }

    @Test(
        arguments: [
            Constants.Dir.childConfigCycle1,
            Constants.Dir.childConfigCycle2,
            Constants.Dir.childConfigCycle3,
            Constants.Dir.parentConfigCycle1,
            Constants.Dir.parentConfigCycle2,
            Constants.Dir.parentConfigCycle3,
        ],
    )
    func configCycleDetection(_ path: URL) {
        CurrentWorkingDirectory.$url.withValue(path) {
            // If the cycle is properly detected, the config should equal the default config.
            #expect(Configuration(configurationFiles: []) == Configuration())
        }
    }

    @Test(.workingDirectory(Constants.Dir.childConfigCycle4))
    func commandLineConfigsCycleDetection() {
        // If the cycle is properly detected, the config should equal the default config.
        assertEqualExceptForFileGraph(
            Configuration(
                configurationFiles: ["main.yml".url(), "child.yml".url()],
                useDefaultConfigOnFailure: true
            ),
            Configuration()
        )
    }

    @Test
    func parentChildOptInAndDisable() {
        struct TestCase: Equatable {
            let optedInInParent: Bool
            let disabledInParent: Bool
            let optedInInChild: Bool
            let disabledInChild: Bool
            let isEnabled: Bool
            var message: String {
                "optedInInParent = \(optedInInParent) " +
                "disabledInParent = \(disabledInParent) " +
                "optedInInChild = \(optedInInChild) " +
                "disabledInChild = \(disabledInChild)"
            }
        }
        let testCases: [TestCase] = [
            // swiftlint:disable line_length
            TestCase(optedInInParent: false, disabledInParent: false, optedInInChild: false, disabledInChild: false, isEnabled: false),
            TestCase(optedInInParent: true, disabledInParent: false, optedInInChild: false, disabledInChild: false, isEnabled: true),
            TestCase(optedInInParent: false, disabledInParent: true, optedInInChild: false, disabledInChild: false, isEnabled: false),
            TestCase(optedInInParent: true, disabledInParent: true, optedInInChild: false, disabledInChild: false, isEnabled: false),
            TestCase(optedInInParent: false, disabledInParent: false, optedInInChild: true, disabledInChild: false, isEnabled: true),
            TestCase(optedInInParent: true, disabledInParent: false, optedInInChild: true, disabledInChild: false, isEnabled: true),
            TestCase(optedInInParent: false, disabledInParent: true, optedInInChild: true, disabledInChild: false, isEnabled: true),
            TestCase(optedInInParent: true, disabledInParent: true, optedInInChild: true, disabledInChild: false, isEnabled: true),
            TestCase(optedInInParent: false, disabledInParent: false, optedInInChild: false, disabledInChild: true, isEnabled: false),
            TestCase(optedInInParent: true, disabledInParent: false, optedInInChild: false, disabledInChild: true, isEnabled: false),
            TestCase(optedInInParent: false, disabledInParent: true, optedInInChild: false, disabledInChild: true, isEnabled: false),
            TestCase(optedInInParent: true, disabledInParent: true, optedInInChild: false, disabledInChild: true, isEnabled: false),
            TestCase(optedInInParent: false, disabledInParent: false, optedInInChild: true, disabledInChild: true, isEnabled: false),
            TestCase(optedInInParent: true, disabledInParent: false, optedInInChild: true, disabledInChild: true, isEnabled: false),
            TestCase(optedInInParent: false, disabledInParent: true, optedInInChild: true, disabledInChild: true, isEnabled: false),
            TestCase(optedInInParent: true, disabledInParent: true, optedInInChild: true, disabledInChild: true, isEnabled: false),
            // swiftlint:enable line_length
        ]
        #expect(testCases.unique.count == 4 * 4)
        let ruleType = ImplicitReturnRule.self
        #expect((ruleType as Any) is any OptInRule.Type)
        let ruleIdentifier = ruleType.identifier
        for testCase in testCases {
            let parentConfiguration = Configuration(rulesMode: .defaultConfiguration(
                disabled: testCase.disabledInParent ? [ruleIdentifier] : [],
                optIn: testCase.optedInInParent ? [ruleIdentifier] : []
            ))
            let childConfiguration = Configuration(rulesMode: .defaultConfiguration(
                disabled: testCase.disabledInChild ? [ruleIdentifier] : [],
                optIn: testCase.optedInInChild ? [ruleIdentifier] : []
            ))
            let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration)
            let isEnabled = mergedConfiguration.contains(rule: ruleType)
            #expect(isEnabled == testCase.isEnabled, Comment(rawValue: testCase.message))
        }
    }

    @Test
    func parentChildDisableForDefaultRule() {
        struct TestCase: Equatable {
            let disabledInParent: Bool
            let disabledInChild: Bool
            let isEnabled: Bool
            var message: String {
                "disabledInParent = \(disabledInParent) disabledInChild = \(disabledInChild)"
            }
        }
        let testCases: [TestCase] = [
            TestCase(disabledInParent: false, disabledInChild: false, isEnabled: true),
            TestCase(disabledInParent: true, disabledInChild: false, isEnabled: false),
            TestCase(disabledInParent: false, disabledInChild: true, isEnabled: false),
            TestCase(disabledInParent: true, disabledInChild: true, isEnabled: false),
        ]
        #expect(testCases.unique.count == 2 * 2)
        let ruleType = BlanketDisableCommandRule.self
        #expect(!(ruleType is any OptInRule.Type))
        let ruleIdentifier = ruleType.identifier
        for testCase in testCases {
            let parentConfiguration = Configuration(
                rulesMode: .defaultConfiguration(disabled: testCase.disabledInParent ? [ruleIdentifier] : [], optIn: [])
            )
            let childConfiguration = Configuration(
                rulesMode: .defaultConfiguration(disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: [])
            )
            let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration)
            let isEnabled = mergedConfiguration.contains(rule: ruleType)
            #expect(isEnabled == testCase.isEnabled, Comment(rawValue: testCase.message))
        }
    }

    @Test
    func parentOnlyRulesAndChildOptInAndDisabled() {
        struct TestCase: Equatable {
            let optedInInChild: Bool
            let disabledInChild: Bool
            let isEnabled: Bool
            var message: String {
                "optedInInChild = \(optedInInChild) disabledInChild = \(disabledInChild)"
            }
        }
        let testCases: [TestCase] = [
            TestCase(optedInInChild: false, disabledInChild: false, isEnabled: true),
            TestCase(optedInInChild: true, disabledInChild: false, isEnabled: true),
            TestCase(optedInInChild: false, disabledInChild: true, isEnabled: false),
            TestCase(optedInInChild: true, disabledInChild: true, isEnabled: false),
        ]
        #expect(testCases.unique.count == 2 * 2)
        let ruleType = ImplicitReturnRule.self
        #expect((ruleType as Any) is any OptInRule.Type)
        let ruleIdentifier = ruleType.identifier
        let parentConfiguration = Configuration(rulesMode: .onlyConfiguration([ruleIdentifier]))
        for testCase in testCases {
            let childConfiguration = Configuration(rulesMode: .defaultConfiguration(
                disabled: testCase.disabledInChild ? [ruleIdentifier] : [],
                optIn: testCase.optedInInChild ? [ruleIdentifier] : []
            ))
            let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration)
            let isEnabled = mergedConfiguration.contains(rule: ruleType)
            #expect(isEnabled == testCase.isEnabled, Comment(rawValue: testCase.message))
        }
    }

    // MARK: Warnings about configurations for disabled rules
    @Test
    func defaultConfigurationDisabledRuleWarnings() {
        let optInRuleType = ImplicitReturnRule.self
        #expect((optInRuleType as Any) is any OptInRule.Type)
        testDefaultConfigurationDisabledRuleWarnings(for: optInRuleType)

        let defaultRuleType = BlockBasedKVORule.self
        #expect(!((defaultRuleType as Any) is any OptInRule.Type))
        testDefaultConfigurationDisabledRuleWarnings(for: defaultRuleType)
    }

    private func testDefaultConfigurationDisabledRuleWarnings(for ruleType: any Rule.Type) {
        let ruleIdentifier = ruleType.identifier

        let parentConfigurations = [
            nil,
            Configuration.emptyDefaultConfiguration(),
            Configuration.optInDefaultConfiguration(ruleIdentifier),
            Configuration.optInDisabledDefaultConfiguration(ruleIdentifier),
            Configuration.disabledDefaultConfiguration(ruleIdentifier),
            Configuration.emptyOnlyConfiguration(),
            Configuration.enabledOnlyConfiguration(ruleIdentifier),
            Configuration.allEnabledConfiguration(),
        ]

        let configurations = [
            Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [])),
            Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [ruleIdentifier])),
            Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [ruleIdentifier])),
            Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [])),
        ]

        for parentConfiguration in parentConfigurations {
            for configuration in configurations {
                testParentConfiguration(parentConfiguration, configuration: configuration, ruleType: ruleType)
            }
        }
    }

    private func testParentConfiguration(
        _ parentConfiguration: Configuration?,
        configuration: Configuration,
        ruleType: any Rule.Type
    ) {
        guard case let .defaultConfiguration(disabledRules, optInRules) = configuration.rulesMode else {
            Issue.record("Configuration rulesMode was not the default")
            return
        }

        let mergedConfiguration = parentConfiguration?.merged(withChild: configuration) ?? configuration
        let isEnabled = mergedConfiguration.contains(rule: ruleType)
        let issue = Configuration.validateConfiguredRuleIsEnabled(
            parentConfiguration: parentConfiguration,
            disabledRules: disabledRules,
            optInRules: optInRules,
            ruleType: ruleType
        )
        #expect(isEnabled == (issue == nil))
        guard let issue else {
            return
        }
        let ruleIdentifier = ruleType.identifier

        guard disabledRules.isEmpty, optInRules.isEmpty else {
            #expect(issue == Issue.ruleDisabledInDisabledRules(ruleID: ruleIdentifier))
            return
        }

        if parentConfiguration == nil ||
            parentConfiguration == Configuration.emptyDefaultConfiguration() {
            #expect(issue == Issue.ruleNotEnabledInOptInRules(ruleID: ruleIdentifier))
        } else if parentConfiguration == Configuration.emptyOnlyConfiguration() {
            if ruleType is any OptInRule.Type {
                #expect(issue == Issue.ruleNotEnabledInOptInRules(ruleID: ruleIdentifier))
            } else {
                #expect(issue == Issue.ruleNotEnabledInParentOnlyRules(ruleID: ruleIdentifier))
            }
        } else if parentConfiguration == Configuration.optInDisabledDefaultConfiguration(ruleIdentifier) ||
            parentConfiguration == Configuration.disabledDefaultConfiguration(ruleIdentifier) {
            #expect(issue == Issue.ruleDisabledInParentConfiguration(ruleID: ruleIdentifier))
        }
    }

    @Test
    func onlyConfigurationDisabledRulesWarnings() {
        let optInRuleType = ImplicitReturnRule.self
        #expect((optInRuleType as Any) is any OptInRule.Type)
        testOnlyConfigurationDisabledRulesWarnings(ruleType: optInRuleType)

        let defaultRuleType = BlockBasedKVORule.self
        #expect(!((defaultRuleType as Any) is any OptInRule.Type))
        testOnlyConfigurationDisabledRulesWarnings(ruleType: defaultRuleType)
    }

    private func testOnlyConfigurationDisabledRulesWarnings(ruleType: any Rule.Type) {
        let issue = Configuration.validateConfiguredRuleIsEnabled(onlyRules: [], ruleType: ruleType)
        #expect(issue == Issue.ruleNotPresentInOnlyRules(ruleID: ruleType.identifier))
        #expect(
            Configuration.validateConfiguredRuleIsEnabled(onlyRules: [ruleType.identifier], ruleType: ruleType) == nil
        )
    }

    // MARK: - Remote Configs
    @Test(.temporaryDirectory(withFixture: Constants.Dir.remoteConfigChild))
    func validRemoteChildConfig() {
        let mockedNetworkResults = [
            "https://www.mock.com":
            """
            included:
              - Test/Test1/Test/Test
              - Test/Test2/Test/Test
            """,
        ]
        let remoteConfig = Configuration.FileGraph.FilePath.$mockedNetworkResults.withValue(mockedNetworkResults) {
            Configuration(configurationFiles: ["main.yml".url()])
        }
        assertEqualExceptForFileGraph(
            remoteConfig,
            Configuration(configurationFiles: ["expected.yml".url()])
        )
    }

    @Test(.temporaryDirectory(withFixture: Constants.Dir.remoteConfigParent))
    func validRemoteParentConfig() {
        let mockedNetworkResults = [
            "https://www.mock.com":
            """
            included:
              - Test/Test1
              - Test/Test2

            excluded:
              - Test/Test1/Test
              - Test/Test2/Test

            line_length: 80
            """,
        ]
        let remoteConfig = Configuration.FileGraph.FilePath.$mockedNetworkResults.withValue(mockedNetworkResults) {
            Configuration(configurationFiles: ["main.yml".url()])
        }
        assertEqualExceptForFileGraph(
            remoteConfig,
            Configuration(configurationFiles: ["expected.yml".url()])
        )
    }

    @Test(.temporaryDirectory(withFixture: Constants.Dir.remoteConfigLocalRef))
    func remoteConfigNotAllowedToReferenceLocalConfig() {
        let mockedNetworkResults = [
            "https://www.mock.com":
            """
            line_length: 60

            child_config: child.yml
            """,
        ]
        let remoteConfig = Configuration.FileGraph.FilePath.$mockedNetworkResults.withValue(mockedNetworkResults) {
            Configuration(configurationFiles: []) // Not specifying a file means the .swiftlint.yml will be used.
        }

        // If the remote file is not allowed to reference a local file, the config should equal the default config.
        #expect(remoteConfig == Configuration())
    }

    @Test(.temporaryDirectory(withFixture: Constants.Dir.remoteConfigCycle))
    func remoteConfigCycleDetection() {
        let mockedNetworkResults = [
            "https://www.mock.com":
            """
            child_config: https://www.mock.com
            """,
        ]
        let remoteConfig = Configuration.FileGraph.FilePath.$mockedNetworkResults.withValue(mockedNetworkResults) {
            Configuration(configurationFiles: []) // Not specifying a file means the .swiftlint.yml will be used.
        }
        // If the cycle is properly detected, the config should equal the default config.
        #expect(remoteConfig == Configuration())
    }

    // MARK: - Helpers
    /// This helper function checks whether two configurations are equal except for their file graph.
    /// This is needed to test a child/parent merged config against an expected config.
    func assertEqualExceptForFileGraph(_ configuration1: Configuration, _ configuration2: Configuration) {
        #expect(
            configuration1.rulesWrapper.disabledRuleIdentifiers == configuration2.rulesWrapper.disabledRuleIdentifiers
        )
        #expect(
            configuration1.rules.map { type(of: $0).identifier } == configuration2.rules.map { type(of: $0).identifier }
        )
        #expect(
            Set(configuration1.rulesWrapper.allRulesWrapped.map {
                $0.rule.createConfigurationDescription().oneLiner()
            }) == Set(configuration2.rulesWrapper.allRulesWrapped.map {
                $0.rule.createConfigurationDescription().oneLiner()
            })
        )
        #expect(configuration1.includedPaths.map(\.path).sorted() == configuration2.includedPaths.map(\.path).sorted())
        #expect(configuration1.excludedPaths.map(\.path).sorted() == configuration2.excludedPaths.map(\.path).sorted())
    }
}

private extension Configuration {
    static func emptyDefaultConfiguration() -> Self {
        Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: []))
    }
    static func optInDefaultConfiguration(_ ruleIdentifier: String) -> Self {
        Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [ruleIdentifier]))
    }
    static func optInDisabledDefaultConfiguration(_ ruleIdentifier: String) -> Self {
        Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [ruleIdentifier]))
    }
    static func disabledDefaultConfiguration(_ ruleIdentifier: String) -> Self {
        Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: []))
    }
    static func emptyOnlyConfiguration() -> Self { Configuration(rulesMode: .onlyConfiguration([])) }
    static func enabledOnlyConfiguration(_ ruleIdentifier: String) -> Self {
        Configuration(rulesMode: .onlyConfiguration([ruleIdentifier]))
    }
    static func allEnabledConfiguration() -> Self { Configuration(rulesMode: .allCommandLine)}
    static func onlyRuleConfiguration(_ ruleIdentifier: String) -> Self {
        Configuration(rulesMode: .onlyCommandLine([ruleIdentifier]))
    }
}
