@testable import SwiftLintBuiltInRules
import TestHelpers
import XCTest

final class ExpiringTodoRuleTests: SwiftLintTestCase {
    func testExpiringTodo() {
        verifyRule(ExpiringTodoRule.description, commentDoesntViolate: false)
    }

    func testExpiredTodo() {
        let example = Example("fatalError() // TODO: [\(dateString(for: .expired))] Implement")
        let violations = violations(example)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations.first!.reason, "TODO/FIXME has expired and must be resolved")
    }

    func testExpiredFixMe() {
        let example = Example("fatalError() // FIXME: [\(dateString(for: .expired))] Implement")
        let violations = violations(example)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations.first!.reason, "TODO/FIXME has expired and must be resolved")
    }

    func testApproachingExpiryTodo() {
        let example = Example("fatalError() // TODO: [\(dateString(for: .approachingExpiry))] Implement")
        let violations = violations(example)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations.first!.reason, "TODO/FIXME is approaching its expiry and should be resolved soon")
    }

    func testNonExpiredTodo() {
        let example = Example("fatalError() // TODO: [\(dateString(for: .badFormatting))] Implement")
        XCTAssertEqual(violations(example).count, 0)
    }

    func testExpiredCustomDelimiters() {
        let ruleConfig = ExpiringTodoConfiguration(
            dateDelimiters: .init(opening: "<", closing: ">")
        )
        let example = Example("fatalError() // TODO: <\(dateString(for: .expired))> Implement")
        let violations = violations(example, ruleConfig)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations.first!.reason, "TODO/FIXME has expired and must be resolved")
    }

    func testExpiredCustomSeparator() {
        let ruleConfig = ExpiringTodoConfiguration(
            dateFormat: "MM-dd-yyyy",
            dateSeparator: "-"
        )
        let example = Example(
            "fatalError() // TODO: [\(dateString(for: .expired, format: ruleConfig.dateFormat))] Implement"
        )
        let violations = violations(example, ruleConfig)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations.first!.reason, "TODO/FIXME has expired and must be resolved")
    }

    func testExpiredCustomFormat() {
        let ruleConfig = ExpiringTodoConfiguration(dateFormat: "yyyy/MM/dd")
        let example = Example(
            "fatalError() // TODO: [\(dateString(for: .expired, format: ruleConfig.dateFormat))] Implement"
        )
        let violations = violations(example, ruleConfig)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations.first!.reason, "TODO/FIXME has expired and must be resolved")
    }

    func testMultipleExpiredTodos() {
        let example = Example(
            """
            fatalError() // TODO: [\(dateString(for: .expired))] Implement one
            fatalError() // TODO: Implement two by [\(dateString(for: .expired))]
            """
        )
        let violations = violations(example)
        XCTAssertEqual(violations.count, 2)
        XCTAssertEqual(violations[0].reason, "TODO/FIXME has expired and must be resolved")
        XCTAssertEqual(violations[0].location.line, 1)
        XCTAssertEqual(violations[1].reason, "TODO/FIXME has expired and must be resolved")
        XCTAssertEqual(violations[1].location.line, 2)
    }

    func testTodoAndExpiredTodo() {
        let example = Example(
            """
            // TODO: Implement one - without deadline
            fatalError()
            // TODO: Implement two by [\(dateString(for: .expired))]
            """
        )
        let violations = violations(example)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations[0].reason, "TODO/FIXME has expired and must be resolved")
        XCTAssertEqual(violations[0].location.line, 3)
    }

    func testMultilineExpiredTodo() {
        let example = Example(
            """
            // TODO: Multi-line task
            //       for: @MATODOLU
            //       deadline: [\(dateString(for: .expired))]
            //       severity: fatal
            """
        )
        let violations = violations(example)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations[0].reason, "TODO/FIXME has expired and must be resolved")
        XCTAssertEqual(violations[0].location.line, 3)
    }

    func testTodoFunctionAndExpiredTodo() {
        let example = Example(
            """
            TODO()
            // TODO: Implement two by [\(dateString(for: .expired))]
            """
        )
        let violations = violations(example)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations[0].reason, "TODO/FIXME has expired and must be resolved")
        XCTAssertEqual(violations[0].location.line, 2)
    }

    func testBadExpiryTodoFormat() {
        let ruleConfig = ExpiringTodoConfiguration(
            dateFormat: "dd/yyyy/MM"
        )
        let example = Example("fatalError() // TODO: [31/01/2020] Implement")
        let violations = violations(example, ruleConfig)
        XCTAssertEqual(violations.count, 1)
        XCTAssertEqual(violations.first?.reason, "Expiring TODO/FIXME is incorrectly formatted")
    }

    private func violations(_ example: Example, _ config: ExpiringTodoConfiguration? = nil) -> [StyleViolation] {
        let config = config ?? ExpiringTodoConfiguration()
        let serializedConfig = [
            "expired_severity": config.expiredSeverity.severity.rawValue,
            "approaching_expiry_severity": config.approachingExpirySeverity.severity.rawValue,
            "bad_formatting_severity": config.badFormattingSeverity.severity.rawValue,
            "approaching_expiry_threshold": config.approachingExpiryThreshold,
            "date_format": config.dateFormat,
            "date_delimiters": [
                "opening": config.dateDelimiters.opening,
                "closing": config.dateDelimiters.closing,
            ],
            "date_separator": config.dateSeparator,
        ] as [String: Any]
        return TestHelpers.violations(example, config: makeConfig(serializedConfig, ExpiringTodoRule.identifier)!)
    }

    private func dateString(for status: ExpiringTodoRule.ExpiryViolationLevel, format: String? = nil) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = format ?? ExpiringTodoConfiguration().dateFormat
        return formatter.string(from: date(for: status))
    }

    private func date(for status: ExpiringTodoRule.ExpiryViolationLevel) -> Date {
        let ruleConfiguration = ExpiringTodoRule().configuration

        let daysToAdvance: Int

        switch status {
        case .approachingExpiry:
            daysToAdvance = ruleConfiguration.approachingExpiryThreshold
        case .expired:
            daysToAdvance = 0
        case .badFormatting:
            daysToAdvance = ruleConfiguration.approachingExpiryThreshold + 1
        }

        return Calendar.current
            .date(
                byAdding: .day,
                value: daysToAdvance,
                to: .init()
            )!
    }
}
