import Foundation
import SourceKittenFramework
import TestHelpers
import XCTest

@testable import SwiftLintBuiltInRules
@testable import SwiftLintFramework

final class ReporterTests: SwiftLintTestCase {
    private let violations = [
        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
        ),
    ]

    func testReporterFromString() {
        for reporter in reportersList {
            XCTAssertEqual(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
    }

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

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

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

    func testGitLabJUnitReporter() 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)")
    }

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

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

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

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

    func testSARIFReporter() 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
            XCTAssertFalse(rules.isEmpty, "Rules array should not be empty")

            // Verify rule structure
            if let firstRule = rules.first {
                XCTAssertNotNil(firstRule["id"], "Rule should have id")
                XCTAssertNotNil(firstRule["shortDescription"], "Rule should have shortDescription")
                XCTAssertNotNil(firstRule["fullDescription"], "Rule should have fullDescription")
                XCTAssertNotNil(firstRule["helpUri"], "Rule should have helpUri")
            }

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

        return json.bridge()
    }

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

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

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

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

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

    func testRelativePathReporterPaths() {
        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])
        XCTAssertFalse(result.contains(relativePath.filepath))
        XCTAssertTrue(result.contains(relativePath.relativeDisplayPath))
    }

    func testSummaryReporter() {
        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(violations + [correctableViolation])
        XCTAssertEqual(result, expectedOutput)
    }

    func testSummaryReporterWithNoViolations() {
        let expectedOutput = stringFromFile("CannedSummaryReporterNoViolationsOutput.txt")
            .trimmingTrailingCharacters(in: .whitespacesAndNewlines)
        let result = SummaryReporter.generateReport([])
        XCTAssertEqual(result, expectedOutput)
    }

    private func assertEqualContent(referenceFile: String,
                                    reporterType: any Reporter.Type,
                                    stringConverter: (String) throws -> some Equatable = \.self,
                                    file: StaticString = #filePath,
                                    line: UInt = #line) 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(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)
        }
        XCTAssertEqual(convertedReference, convertedReporterOutput, file: file, line: line)
    }
}
