//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
@testable import SignalServiceKit
import XCTest

class NSAttributedStringInGroupUpdateCopyTest: SSKBaseTest {
    private func assertEqual(_ one: NSAttributedString, _ two: NSAttributedString) {
        XCTAssertEqual(one, two)
    }

    func test_XMadeYAnAdmin() {
        assertEqual(.aliceMadeBobAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .bob))
        assertEqual(.aliceHMadeAliceAdmin, NSAttributedString.make(firstUser: .aliceH, madeAdmin: .alice))
        assertEqual(.aliceMadeMadeAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .made))
        assertEqual(.aliceMadeAnotherAliceAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .anotherAlice))
        assertEqual(.aliceMadeSingleArgFormatSpecifierAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .singleArgFormatSpecifier))
        assertEqual(.aliceMadeMultiArgFormatSpecifierAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .multiArgFormatSpecifier))
        assertEqual(.aliceMadeEmptyAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .empty))
    }

    func test_XMadeYAnAdmin_ArabicNameInEnglishString() {
        assertEqual(.aliceMadeFatimaInArabicAnAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .fatima))
    }

    func test_XMadeYAnAdmin_EnglishNameInArabicString() {
        assertEqual(.aliceMadeBobAdmin_Arabic, NSAttributedString.makeArabic(firstUser: .alice, madeAdmin: .bob))
    }

    func test_XMadeYAnAdmin_ChineseNameInEnglishString() {
        assertEqual(.qiInChineseMadeAliceAdmin, NSAttributedString.make(firstUser: .qi, madeAdmin: .alice))
    }

    func test_XMadeYAnAdmin_ChineseAndArabicNamesInArabicString() {
        assertEqual(.qiInChineseMadeFatimaInArabicAdmin_Arabic, NSAttributedString.makeArabic(firstUser: .qi, madeAdmin: .fatima))
    }

    /// Note that this test involves formatting numbers into a format string,
    /// which is done in the guts of Signal using the "current locale".
    /// Therefore, running this test outside of the "United States" locale in
    /// English will fail due to substitution errors.
    func test_XInvitedNPeople() throws {
        try XCTSkipIf(Locale.current.identifier != "en_US", "This test requires the en_US locale for number formatting.")

        assertEqual(.aliceInvitedOnePerson, NSAttributedString.make(user: .alice, invitedNPeople: 1))
        assertEqual(.aliceInvitedTwoPeople, NSAttributedString.make(user: .alice, invitedNPeople: 2))
        assertEqual(.aliceInvitedOneMillionPeople, NSAttributedString.make(user: .alice, invitedNPeople: 1000000))
        assertEqual(.emptyInvitedThreePeople, NSAttributedString.make(user: .empty, invitedNPeople: 3))
    }

    /// Technically this tests a string that is never used in group updates.
    /// However, we don't have any relevant strings that have the name at the
    /// end of the string, and I wanted to test that.
    func test_NameAtEndOfString() {
        assertEqual(
            .aliceSomethingWithBobsNameAtEndOfString,
            NSAttributedString.makeWithNameAtEndOfString(firstUser: .alice, userAtEndOfString: .bob, lang: "en")
        )

        assertEqual(
            .aliceSomethingWithBobsNameAtEndOfString_Arabic,
            NSAttributedString.makeWithNameAtEndOfString(firstUser: .alice, userAtEndOfString: .bob, lang: "ar")
        )
    }

    /// Some localized strings invert the "expected order" of format args, e.g.
    /// `%2$@` may be placed before `%1$@` for a given language simply based on
    /// the grammar of that language. This tests for that, using a string that
    /// is known to have this behavior in Malayalam.
    func test_LocalizedStringInvertsFormatArgOrder() {
        assertEqual(
            .aliceSomethingBobWithInvertedNameOrder,
            NSAttributedString.makeWithInvertedFormatArgOrder(firstUser: .alice, secondUser: .bob)
        )
    }
}

// MARK: - "X made Y an admin"

private extension NSAttributedString {
    static let aliceMadeBobAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .bob) an admin.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.bob, toRange: NSRange(location: 13, length: 5))

        return mutable
    }()

    static let aliceHMadeAliceAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .aliceH) made \(user: .alice) an admin.")
        mutable.addAddress(.aliceH, toRange: NSRange(location: 0, length: 9))
        mutable.addAddress(.alice, toRange: NSRange(location: 15, length: 7))

        return mutable
    }()

    static let aliceMadeMadeAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .made) an admin.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.made, toRange: NSRange(location: 13, length: 6))

        return mutable
    }()

    static let aliceMadeAnotherAliceAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .anotherAlice) an admin.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.anotherAlice, toRange: NSRange(location: 13, length: 7))

        return mutable
    }()

    static let aliceMadeSingleArgFormatSpecifierAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .singleArgFormatSpecifier) an admin.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.singleArgFormatSpecifier, toRange: NSRange(location: 13, length: 4))

        return mutable
    }()

    static let aliceMadeMultiArgFormatSpecifierAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .multiArgFormatSpecifier) an admin.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.multiArgFormatSpecifier, toRange: NSRange(location: 13, length: 6))

        return mutable
    }()

    /// This should theoretically be impossible - we should never in practice
    /// have an empty substitution. However, if we do, we don't want to fail
    /// here :)
    static let aliceMadeEmptyAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .empty) an admin.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.empty, toRange: NSRange(location: 13, length: 2))

        return mutable
    }()

    static func make(firstUser: ReferencedUser, madeAdmin secondUser: ReferencedUser) -> NSAttributedString {
        make(
            fromFormat: .xMadeYAnAdminFormat,
            groupUpdateFormatArgs: [
                .name(firstUser.name, firstUser.address),
                .name(secondUser.name, secondUser.address)
            ]
        )
    }
}

// MARK: - "X made Y an admin", but using non-English languages

private extension NSAttributedString {
    static let aliceMadeBobAdmin_Arabic: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "إنَّ \(user: .alice) قد جعلَ \(user: .bob) مُشرفاً.")
        mutable.addAddress(.alice, toRange: NSRange(location: 5, length: 7))
        mutable.addAddress(.bob, toRange: NSRange(location: 21, length: 5))

        return mutable
    }()

    static let aliceMadeFatimaInArabicAnAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .fatima) an admin.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.fatima, toRange: NSRange(location: 13, length: 10))

        return mutable
    }()

    static let qiInChineseMadeAliceAdmin: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .qi) made \(user: .alice) an admin.")
        mutable.addAddress(.qi, toRange: NSRange(location: 0, length: 3))
        mutable.addAddress(.alice, toRange: NSRange(location: 9, length: 7))

        return mutable
    }()

    static let qiInChineseMadeFatimaInArabicAdmin_Arabic: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "إنَّ \(user: .qi) قد جعلَ \(user: .fatima) مُشرفاً.")
        mutable.addAddress(.qi, toRange: NSRange(location: 5, length: 3))
        mutable.addAddress(.fatima, toRange: NSRange(location: 17, length: 10))

        return mutable
    }()

    static func makeArabic(firstUser: ReferencedUser, madeAdmin secondUser: ReferencedUser) -> NSAttributedString {
        make(
            fromFormat: .xMadeYAnAdminFormat_Arabic,
            groupUpdateFormatArgs: [
                .name(firstUser.name, firstUser.address),
                .name(secondUser.name, secondUser.address)
            ]
        )
    }
}

// MARK: - "X invited N people"

private extension NSAttributedString {
    static let aliceInvitedOnePerson: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 1 person to the group.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))

        return mutable
    }()

    static let aliceInvitedTwoPeople: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 2 people to the group.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))

        return mutable
    }()

    static let aliceInvitedOneMillionPeople: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 1,000,000 people to the group.")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))

        return mutable
    }()

    static let emptyInvitedThreePeople: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .empty) invited 3 people to the group.")
        mutable.addAddress(.empty, toRange: NSRange(location: 0, length: 2))

        return mutable
    }()

    static func make(user: ReferencedUser, invitedNPeople: Int) -> NSAttributedString {
        make(
            fromFormat: .xInvitedNPeopleFormat,
            groupUpdateFormatArgs: [.raw(invitedNPeople), .name(user.name, user.address)]
        )
    }
}

// MARK: - Name at end of string

private extension NSAttributedString {
    static let aliceSomethingWithBobsNameAtEndOfString: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) to \(user: .bob)")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.bob, toRange: NSRange(location: 11, length: 5))

        return mutable
    }()

    static let aliceSomethingWithBobsNameAtEndOfString_Arabic: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "\(user: .alice) إلى \(user: .bob)")
        mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
        mutable.addAddress(.bob, toRange: NSRange(location: 12, length: 5))

        return mutable
    }()

    static func makeWithNameAtEndOfString(
        firstUser: ReferencedUser,
        userAtEndOfString otherUser: ReferencedUser,
        lang: String
    ) -> NSAttributedString {
        make(
            fromFormat: String.newGroupMessageNotificationTitleFormat(lang: lang),
            groupUpdateFormatArgs: [.name(firstUser.name, firstUser.address), .name(otherUser.name, otherUser.address)]
        )
    }
}

// MARK: - Localized string inverts format arg order

private extension NSAttributedString {
    static let aliceSomethingBobWithInvertedNameOrder: NSAttributedString = {
        let mutable = NSMutableAttributedString(string: "“\(user: .bob)”-এ “\(user: .alice)” যোগ করবেন?")
        mutable.addAddress(.bob, toRange: NSRange(location: 1, length: 5))
        mutable.addAddress(.alice, toRange: NSRange(location: 11, length: 7))

        return mutable
    }()

    /// The returned string formats the "second" user passed before the "first"
    /// user, simply as a result of the format string.
    static func makeWithInvertedFormatArgOrder(firstUser: ReferencedUser, secondUser: ReferencedUser) -> NSAttributedString {
        make(
            fromFormat: .addToGroupActionSheetMessage_Bangla,
            groupUpdateFormatArgs: [.name(firstUser.name, firstUser.address), .name(secondUser.name, secondUser.address)]
        )
    }
}

// MARK: - Add addresses

private extension NSMutableAttributedString {
    func addAddress(_ address: SignalServiceAddress, toRange range: NSRange) {
        addAttribute(.addressOfName, value: address, range: range)
    }
}

// MARK: - Users

private extension SignalServiceAddress {
    static let alice = SignalServiceAddress(phoneNumber: "+17735550100")
    static let aliceH = SignalServiceAddress(phoneNumber: "+17735550101")
    static let bob = SignalServiceAddress(phoneNumber: "+17735550102")
    static let made = SignalServiceAddress(phoneNumber: "+17735550103")
    static let anotherAlice = SignalServiceAddress(phoneNumber: "+17735550104")
    static let singleArgFormatSpecifier = SignalServiceAddress(phoneNumber: "+17735550105")
    static let multiArgFormatSpecifier = SignalServiceAddress(phoneNumber: "+17735550106")
    static let empty = SignalServiceAddress(phoneNumber: "+17735550107")
    static let fatima = SignalServiceAddress(phoneNumber: "+17735550108")
    static let qi = SignalServiceAddress(phoneNumber: "+17735550109")
}

private struct ReferencedUser {
    static let alice: ReferencedUser = ReferencedUser(name: "Alice", address: .alice)
    static let aliceH: ReferencedUser = ReferencedUser(name: "Alice H", address: .aliceH)
    static let bob: ReferencedUser = ReferencedUser(name: "Bob", address: .bob)
    static let made: ReferencedUser = ReferencedUser(name: "made", address: .made)
    static let anotherAlice: ReferencedUser = ReferencedUser(name: "Alice", address: .anotherAlice)
    static let singleArgFormatSpecifier: ReferencedUser = ReferencedUser(name: "%@", address: .singleArgFormatSpecifier)
    static let multiArgFormatSpecifier: ReferencedUser = ReferencedUser(name: "%1$@", address: .multiArgFormatSpecifier)
    static let empty: ReferencedUser = ReferencedUser(name: "", address: .empty)
    static let fatima: ReferencedUser = ReferencedUser(name: "فَاطِمَة", address: .fatima)
    static let qi: ReferencedUser = ReferencedUser(name: "琦", address: .qi)

    let name: String
    let address: SignalServiceAddress
}

private extension String.StringInterpolation {
    /// We wrap all formatted names in Unicode isolates.
    mutating func appendInterpolation(user: ReferencedUser) {
        appendLiteral("\u{2068}\(user.name)\u{2069}")
    }
}

// MARK: - Localized format strings

/// We should ideally use ``OWSLocalizedString`` for this, but it doesn't
/// support specifying the locale (uses the current).
private extension String {
    static let xMadeYAnAdminFormat: String = {
        try! localized(key: "GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT")
    }()

    static let xMadeYAnAdminFormat_Arabic: String = {
        try! localized(key: "GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT", lang: "ar")
    }()

    static let xInvitedNPeopleFormat: String = {
        try! localized(key: "GROUP_REMOTE_USER_INVITED_BY_REMOTE_USER_%d", tableName: "PluralAware")
    }()

    static func newGroupMessageNotificationTitleFormat(lang: String) -> String {
        try! localized(key: "NEW_GROUP_MESSAGE_NOTIFICATION_TITLE", lang: lang)
    }

    static let addToGroupActionSheetMessage_Bangla: String = {
        try! localized(key: "ADD_TO_GROUP_ACTION_SHEET_MESSAGE_FORMAT", lang: "bn")
    }()

    static func localized(
        key: String,
        tableName: String? = nil,
        lang: String = "en"
    ) throws -> String {
        guard
            let bundlePath = Bundle.main.path(forResource: lang, ofType: "lproj"),
            let bundle = Bundle(path: bundlePath)
        else {
            throw OWSAssertionError("Failed to find bundle for lang code \(lang)")
        }

        return NSLocalizedString(
            key,
            tableName: tableName,
            bundle: bundle,
            value: "",
            comment: ""
        )
    }
}
