import Foundation
import SourceKittenFramework
import TestHelpers
import Testing

@testable import SwiftLintBuiltInRules
@testable import SwiftLintFramework

@Suite(.rulesRegistered)
struct ReporterTests {
    private static var violations: [StyleViolation] {
        [
            StyleViolation(
                ruleDescription: LineLengthRule.description,
                location: Location(file: URL.cwd.appending(path: "filename"), line: 1, character: 1),
                reason: "Violation Reason 1"
            ),
            StyleViolation(
                ruleDescription: LineLengthRule.description,
                severity: .error,
                location: Location(file: URL.cwd.appending(path: "filename"), line: 1),
                reason: "Violation Reason 2"
            ),
            StyleViolation(
                ruleDescription: SyntacticSugarRule.description,
                severity: .error,
                location: Location(
                    file: URL.cwd
                        .appending(path: "path", directoryHint: .isDirectory)
                        .appending(path: "file.swift", directoryHint: .notDirectory),
                    line: 1,
                    character: 2
                ),
                reason: "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array<Int>"),
            StyleViolation(
                ruleDescription: ColonRule.description,
                severity: .error,
                location: Location(file: nil),
                reason: nil
            ),
        ]
    }

    @Test
    func reporterFromString() {
        for reporter in reportersList {
            #expect(reporter.identifier == reporterFrom(identifier: reporter.identifier).identifier)
        }
    }

    private func stringFromFile(_ filename: String) -> String {
        let path = TestResources.path().appending(path: filename, directoryHint: .notDirectory)
        return SwiftLintFile(path: path)!.contents
    }

    @Test
    func xcodeReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedXcodeReporterOutput.txt",
            reporterType: XcodeReporter.self
        )
    }

    @Test
    func emojiReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedEmojiReporterOutput.txt",
            reporterType: EmojiReporter.self
        )
    }

    @Test
    func gitHubActionsLoggingReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedGitHubActionsLoggingReporterOutput.txt",
            reporterType: GitHubActionsLoggingReporter.self
        )
    }

    @Test
    func gitLabJUnitReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedGitLabJUnitReporterOutput.xml",
            reporterType: GitLabJUnitReporter.self
        )
    }

    private func jsonValue(_ jsonString: String) throws -> NSObject {
        let data = jsonString.data(using: .utf8)!
        let result = try JSONSerialization.jsonObject(with: data, options: [])
        if let dict = (result as? [String: Any])?.bridge() {
            return dict
        }
        if let array = (result as? [Any])?.bridge() {
            return array
        }
        queuedFatalError("Unexpected value in JSON: \(result)")
    }

    @Test
    func jsonReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedJSONReporterOutput.json",
            reporterType: JSONReporter.self,
            stringConverter: { try jsonValue($0) }
        )
    }

    @Test
    func cSVReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedCSVReporterOutput.csv",
            reporterType: CSVReporter.self
        )
    }

    @Test
    func checkstyleReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedCheckstyleReporterOutput.xml",
            reporterType: CheckstyleReporter.self
        )
    }

    @Test
    func codeClimateReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedCodeClimateReporterOutput.json",
            reporterType: CodeClimateReporter.self
        )
    }

    @Test
    func sarifReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedSARIFReporterOutput.sarif",
            reporterType: SARIFReporter.self,
            stringConverter: { try sarifValueWithoutRules($0) }
        )
    }

    private func sarifValueWithoutRules(_ jsonString: String) throws -> NSObject {
        let data = jsonString.data(using: .utf8)!
        guard var json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
            queuedFatalError("Expected dictionary in SARIF JSON")
        }

        // Extract and verify rules before removing them
        if var runs = json["runs"] as? [[String: Any]],
           var firstRun = runs.first,
           var tool = firstRun["tool"] as? [String: Any],
           var driver = tool["driver"] as? [String: Any],
           let rules = driver["rules"] as? [[String: Any]] {
            // Verify rules array is not empty
            #expect(!rules.isEmpty)

            // Verify rule structure
            if let firstRule = rules.first {
                #expect(firstRule["id"] != nil)
                #expect(firstRule["shortDescription"] != nil)
                #expect(firstRule["fullDescription"] != nil)
                #expect(firstRule["helpUri"] != nil)
            }

            // Remove rules array for comparison
            driver.removeValue(forKey: "rules")
            tool["driver"] = driver
            firstRun["tool"] = tool
            runs[0] = firstRun
            json["runs"] = runs
        }

        return json.bridge()
    }

    @Test
    func junitReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedJunitReporterOutput.xml",
            reporterType: JUnitReporter.self
        )
    }

    @Test
    func htmlReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedHTMLReporterOutput.html",
            reporterType: HTMLReporter.self
        )
    }

    @Test
    func sonarQubeReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedSonarQubeReporterOutput.json",
            reporterType: SonarQubeReporter.self,
            stringConverter: { try jsonValue($0) }
        )
    }

    @Test
    func markdownReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedMarkdownReporterOutput.md",
            reporterType: MarkdownReporter.self
        )
    }

    @Test
    func relativePathReporter() throws {
        try assertEqualContent(
            referenceFile: "CannedRelativePathReporterOutput.txt",
            reporterType: RelativePathReporter.self
        )
    }

    @Test
    func relativePathReporterPaths() {
        let relativePath = "filename".url()
        let location = Location(file: relativePath, line: 1, character: 2)
        let violation = StyleViolation(ruleDescription: LineLengthRule.description,
                                       location: location,
                                       reason: "Violation Reason")
        let result = RelativePathReporter.generateReport([violation])
        #expect(!result.contains(relativePath.filepath))
        #expect(result.contains(relativePath.relativeDisplayPath))
    }

    @Test
    func summaryReporter() {
        let expectedOutput = stringFromFile("CannedSummaryReporterOutput.txt")
            .trimmingTrailingCharacters(in: .whitespacesAndNewlines)
        let correctableViolation = StyleViolation(
            ruleDescription: VerticalWhitespaceOpeningBracesRule.description,
            location: Location(file: URL.cwd.appending(path: "filename"), line: 1, character: 2),
            reason: "Violation Reason"
        )
        let result = SummaryReporter.generateReport(Self.violations + [correctableViolation])
        #expect(result == expectedOutput)
    }

    @Test
    func summaryReporterWithNoViolations() {
        let expectedOutput = stringFromFile("CannedSummaryReporterNoViolationsOutput.txt")
            .trimmingTrailingCharacters(in: .whitespacesAndNewlines)
        let result = SummaryReporter.generateReport([])
        #expect(result == expectedOutput)
    }

    private func assertEqualContent(
        referenceFile: String,
        reporterType: any Reporter.Type,
        stringConverter: (String) throws -> some Equatable = \.self,
        sourceLocation: Testing.SourceLocation = #_sourceLocation
    ) throws {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .short

        let reference = stringFromFile(referenceFile).replacingOccurrences(
            of: "${CURRENT_WORKING_DIRECTORY}",
            with: URL.cwd.path
        ).replacingOccurrences(
            of: "${SWIFTLINT_VERSION}",
            with: SwiftLintFramework.Version.current.value
        ).replacingOccurrences(
            of: "${TODAYS_DATE}",
            with: dateFormatter.string(from: Date())
        )
        let reporterOutput = reporterType.generateReport(Self.violations)
        let convertedReference = try stringConverter(reference)
        let convertedReporterOutput = try stringConverter(reporterOutput)
        if convertedReference != convertedReporterOutput {
            let referenceURL = TestResources.path().appending(path: referenceFile, directoryHint: .notDirectory)
            try reporterOutput.replacingOccurrences(
                of: URL.cwd.path,
                with: "${CURRENT_WORKING_DIRECTORY}"
            ).replacingOccurrences(
                of: SwiftLintFramework.Version.current.value,
                with: "${SWIFTLINT_VERSION}"
            ).replacingOccurrences(
                of: dateFormatter.string(from: Date()),
                with: "${TODAYS_DATE}"
            )
            .write(to: referenceURL, atomically: true, encoding: .utf8)
        }
        #expect(
            convertedReference == convertedReporterOutput,
            sourceLocation: sourceLocation
        )
    }
}
