//
//  AnalyticsGeneratorTests.swift
//
//
//  Created by Thibaut Schmitt on 06/09/2022.
//

// CPD-OFF

import Foundation
import XCTest
import ToolCore

@testable import ResgenSwift

final class AnalyticsGeneratorTests: XCTestCase {

    private func getAnalyticsDefinition(
        id: String,
        path: String = "",
        action: String = "",
        category: String = "",
        name: String,
        type: AnalyticsDefinition.TagType,
        tags: [String]
    ) -> AnalyticsDefinition {
        let definition = AnalyticsDefinition(id: id, name: name, type: type)
        definition.tags = tags
        definition.path = path
        definition.action = action
        definition.category = category
        return definition
    }

    private func protocolString(visibility: ExtensionVisibility) -> String {
        """
        // MARK: - Protocol
        
        \(visibility) protocol AnalyticsManagerProtocol {
        
            func logScreen(
                name: String,
                path: String,
                params: [String: Any]?
            )
        
            func logEvent(
                name: String,
                action: String,
                category: String,
                params: [String: Any]?
            )
        
            func setEnable(_ enable: Bool)
        }
        """
    }

    private func firebaseString() -> String {
        """
        // MARK: - Firebase

        class FirebaseAnalyticsManager: AnalyticsManagerProtocol {

            // MARK: - Methods

            func logScreen(
                name: String,
                path: String,
                params: [String: Any]?
            ) {
                var parameters = [
                    AnalyticsParameterScreenName: name as NSObject
                ]

                if path.isEmpty == false {
                    parameters["path"] = path + "/iOS" as NSObject
                }

                if let supplementaryParameters = params {
                    for (newKey, newValue) in supplementaryParameters {
                        if parameters.contains(where: { (key: String, value: NSObject) in
                            key == newKey
                        }) {
                            continue
                        }

                        parameters[newKey] = newValue as? NSObject
                    }
                }

                Analytics.logEvent(
                    AnalyticsEventScreenView,
                    parameters: parameters
                )
            }
        
            func logEvent(
                name: String,
                action: String,
                category: String,
                params: [String: Any]?
            ) {
                var parameters: [String: NSObject] = [
                    AnalyticsParameterItemName: name.replacingOccurrences(of: " ", with: "_") as NSObject
                ]

                if category.isEmpty == false {
                    parameters["AnalyticsParameterItemCategory"] = category as NSObject
                }

                if action.isEmpty == false {
                    parameters["action"] = action as NSObject
                }

                if let supplementaryParameters = params {
                    for (newKey, newValue) in supplementaryParameters {
                        if parameters.contains(where: { (key: String, value: NSObject) in
                            key == newKey
                        }) {
                            continue
                        }

                        parameters[newKey] = newValue as? NSObject
                    }
                }

                Analytics.logEvent(
                    AnalyticsEventSelectContent,
                    parameters: parameters
                )
            }

            func setEnable(_ enable: Bool) {
                Analytics.setAnalyticsCollectionEnabled(enable)
            }
        }
        """
    }

    private func matomoString() -> String {
        """
        // MARK: - Matomo

        class MatomoAnalyticsManager: AnalyticsManagerProtocol {

            // MARK: - Properties

            private var tracker: MatomoTracker

            // MARK: - Init

            init(siteId: String, url: String) {
                debugPrint("[Matomo service] Server URL: \\(url)")
                debugPrint("[Matomo service] Site ID: \\(siteId)")
                tracker = MatomoTracker(
                    siteId: siteId,
                    baseURL: URL(string: url)!
                )

                #if DEBUG
                    tracker.dispatchInterval = 5
                #endif

                #if DEBUG
                    tracker.logger = DefaultLogger(minLevel: .verbose)
                #endif

                debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")")
                debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)")
            }

            // MARK: - Methods

            func logScreen(
                name: String,
                path: String,
                params: [String: Any]?
            ) {
                guard let trackerUrl = tracker.contentBase?.absoluteString else { return }

                let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS")
                tracker.track(
                    view: [name],
                    url: urlString
                )
            }

            func logEvent(
                name: String,
                action: String,
                category: String,
                params: [String: Any]?
            ) {
                tracker.track(
                    eventWithCategory: category,
                    action: action,
                    name: name,
                    number: nil,
                    url: nil
                )
            }

            func setEnable(_ enable: Bool) {
                tracker.isOptedOut = !enable
            }
        }
        """
    }

    func testGeneratedExtensionContentFirebase() {
        // Given
        let sectionOne = AnalyticsCategory(id: "section_one")
        sectionOne.definitions = [
            getAnalyticsDefinition(id: "s1_def_one", name: "s1 def one", type: .screen, tags: ["ios", "iosonly"]),
            getAnalyticsDefinition(id: "s1_def_two", name: "s1 def two", type: .event, tags: ["ios", "iosonly"]),
        ]

        let sectionTwo = AnalyticsCategory(id: "section_two")
        sectionTwo.definitions = [
            getAnalyticsDefinition(id: "s2_def_one", name: "s2 def one", type: .screen, tags: ["ios","iosonly"]),
            getAnalyticsDefinition(id: "s2_def_two", name: "s2 def two", type: .event, tags: ["droid","droidonly"]),
        ]

        let sectionThree = AnalyticsCategory(id: "section_three")
        sectionThree.definitions = [
            getAnalyticsDefinition(id: "s3_def_one", name: "s3 def one", type: .screen, tags: ["droid","droidonly"]),
            getAnalyticsDefinition(id: "s3_def_two", name: "s3 def two", type: .event, tags: ["droid","droidonly"]),
        ]

        // When
        let extensionContent = AnalyticsGenerator.getExtensionContent(
            targets: [TrackerType.firebase],
            sections: [sectionOne, sectionTwo, sectionThree],
            tags: ["ios", "iosonly"],
            isStatic: false,
            visibility: .internal
        )

        // Expect Analytics
        let expect = """
        // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion)

        import Foundation
        import FirebaseAnalytics
        import os

        \(protocolString(visibility: .internal))

        \(firebaseString())

        // MARK: - Traker Type

        internal enum TrackerType: CaseIterable {

            case firebase
        }

        // MARK: - Manager

        internal class AnalyticsManager {

            internal static var shared = AnalyticsManager()

            private init() {}

            // MARK: - Properties

            var managers: [TrackerType: AnalyticsManagerProtocol] = [:]
        
            private var isDebugMode: Bool = false

            private var isEnabled: Bool {
                if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
                    false
                } else {
                    true
                }
            }
        
            private let logger = Logger(subsystem: "resgen", category: "analytics")

            // MARK: - Enable Methods

            private func setAnalytics(enable: Bool, _ analytics: [TrackerType]) {
                managers.forEach { (key, value) in
                    if analytics.contains(where: { type in
                        type == key
                    }) {
                        value.setEnable(enable)
                    }
                }
            }

            internal func enableAnalytics(_ analytics: [TrackerType] = TrackerType.allCases) {
                setAnalytics(enable: true, analytics)
            }

            internal func disableAnalytics(_ analytics: [TrackerType] = TrackerType.allCases) {
                setAnalytics(enable: false, analytics)
            }

            internal func configure(isDebugMode: Bool = false) {
                self.isDebugMode = isDebugMode
                managers[TrackerType.firebase] = FirebaseAnalyticsManager()
            }

            // MARK: - Private Log Methods
        
            private func formattedParams(_ params: [String: Any]?) -> String {
                guard let params = params, !params.isEmpty else { return "-" }
                
                let formattedParams = params.map { key, value in
                    "       \\(key): \\(value)"
                }
                .joined(separator: "\\n")
                
                return "\\n" + formattedParams
            }

            private func logScreen(
                name: String,
                path: String,
                params: [String: Any]?
            ) {
                guard isEnabled else {
                    if isDebugMode {
                        logger.log("Analytics disabled")
                    }
                    return 
                }

                managers.values.forEach { manager in
            
                    if isDebugMode {
                        logger.debug(\"""
                        🖥️ Screen:
                             Name: \\(name, privacy: .public)
                             Path: \\(path, privacy: .public)
                             Params: \\(self.formattedParams(params), privacy: .public)
                        \""")
                    }
        
                    manager.logScreen(
                        name: name,
                        path: path,
                        params: params
                    )
                }
            }

            private func logEvent(
                name: String,
                action: String,
                category: String,
                params: [String: Any]?
            ) {
                guard isEnabled else {
                    if isDebugMode {
                        logger.log("Analytics disabled")
                    }
                    return 
                }

                managers.values.forEach { manager in
            
                    if isDebugMode {
                        logger.debug(\"""
                        📊 Event:
                             Name: \\(name, privacy: .public)
                             Action: \\(action.isEmpty ? "-" : action, privacy: .public)
                             Category: \\(category.isEmpty ? "-" : category, privacy: .public)
                             Params: \\(self.formattedParams(params), privacy: .public)
                        \""")
                    }
        
                    manager.logEvent(
                        name: name,
                        action: action,
                        category: category,
                        params: params
                    )
                }
            }

            // MARK: - section_one

            internal func logScreenS1DefOne() {
                logScreen(
                    name: "s1 def one",
                    path: "",
                    params: nil
                )
            }

            internal func logEventS1DefTwo() {
                logEvent(
                    name: "s1 def two",
                    action: "",
                    category: "",
                    params: nil
                )
            }

            // MARK: - section_two

            internal func logScreenS2DefOne() {
                logScreen(
                    name: "s2 def one",
                    path: "",
                    params: nil
                )
            }
        }

        """

        if extensionContent != expect {
            print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
        }
        XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
    }

    func testGeneratedExtensionContentMatomo() {
        // Given
        let sectionOne = AnalyticsCategory(id: "section_one")
        sectionOne.definitions = [
            getAnalyticsDefinition(id: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: .screen, tags: ["ios", "iosonly"]),
            getAnalyticsDefinition(id: "s1_def_two", action: "test", category: "test", name: "s1 def two", type: .event, tags: ["ios", "iosonly"]),
        ]

        let sectionTwo = AnalyticsCategory(id: "section_two")
        sectionTwo.definitions = [
            getAnalyticsDefinition(id: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: .screen, tags: ["ios","iosonly"]),
            getAnalyticsDefinition(id: "s2_def_two", action: "test", category: "test", name: "s2 def two", type: .event, tags: ["droid","droidonly"]),
        ]

        let sectionThree = AnalyticsCategory(id: "section_three")
        sectionThree.definitions = [
            getAnalyticsDefinition(id: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: .screen, tags: ["droid","droidonly"]),
            getAnalyticsDefinition(id: "s3_def_two", action: "test", category: "test", name: "s3 def two", type: .event, tags: ["droid","droidonly"]),
        ]

        // When
        let extensionContent = AnalyticsGenerator.getExtensionContent(
            targets: [TrackerType.matomo],
            sections: [sectionOne, sectionTwo, sectionThree],
            tags: ["ios", "iosonly"],
            isStatic: false,
            visibility: .internal
        )
        // Expect Analytics
        let expect = """
        // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion)

        import Foundation
        import MatomoTracker
        import os

        \(protocolString(visibility: .internal))

        \(matomoString())

        // MARK: - Traker Type

        internal enum TrackerType: CaseIterable {

            case matomo
        }

        // MARK: - Manager

        internal class AnalyticsManager {

            internal static var shared = AnalyticsManager()

            private init() {}

            // MARK: - Properties

            var managers: [TrackerType: AnalyticsManagerProtocol] = [:]
        
            private var isDebugMode: Bool = false

            private var isEnabled: Bool {
                if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
                    false
                } else {
                    true
                }
            }
        
            private let logger = Logger(subsystem: "resgen", category: "analytics")

            // MARK: - Enable Methods

            private func setAnalytics(enable: Bool, _ analytics: [TrackerType]) {
                managers.forEach { (key, value) in
                    if analytics.contains(where: { type in
                        type == key
                    }) {
                        value.setEnable(enable)
                    }
                }
            }

            internal func enableAnalytics(_ analytics: [TrackerType] = TrackerType.allCases) {
                setAnalytics(enable: true, analytics)
            }

            internal func disableAnalytics(_ analytics: [TrackerType] = TrackerType.allCases) {
                setAnalytics(enable: false, analytics)
            }

            internal func configure(siteId: String, url: String, isDebugMode: Bool = false) {
                self.isDebugMode = isDebugMode
                managers[TrackerType.matomo] = MatomoAnalyticsManager(
                    siteId: siteId,
                    url: url
                )
            }

            // MARK: - Private Log Methods

            private func formattedParams(_ params: [String: Any]?) -> String {
                guard let params = params, !params.isEmpty else { return "-" }
                
                let formattedParams = params.map { key, value in
                    "       \\(key): \\(value)"
                }
                .joined(separator: "\\n")
                
                return "\\n" + formattedParams
            }

            private func logScreen(
                name: String,
                path: String,
                params: [String: Any]?
            ) {
                guard isEnabled else {
                    if isDebugMode {
                        logger.log("Analytics disabled")
                    }
                    return 
                }
        
                managers.values.forEach { manager in
            
                    if isDebugMode {
                        logger.debug(\"""
                        🖥️ Screen:
                             Name: \\(name, privacy: .public)
                             Path: \\(path, privacy: .public)
                             Params: \\(self.formattedParams(params), privacy: .public)
                        \""")
                    }
        
                    manager.logScreen(
                        name: name,
                        path: path,
                        params: params
                    )
                }
            }
        
            private func logEvent(
                name: String,
                action: String,
                category: String,
                params: [String: Any]?
            ) {
                guard isEnabled else {
                    if isDebugMode {
                        logger.log("Analytics disabled")
                    }
                    return 
                }
        
                managers.values.forEach { manager in
            
                    if isDebugMode {
                        logger.debug(\"""
                        📊 Event:
                             Name: \\(name, privacy: .public)
                             Action: \\(action.isEmpty ? "-" : action, privacy: .public)
                             Category: \\(category.isEmpty ? "-" : category, privacy: .public)
                             Params: \\(self.formattedParams(params), privacy: .public)
                        \""")
                    }
        
                    manager.logEvent(
                        name: name,
                        action: action,
                        category: category,
                        params: params
                    )
                }
            }
        
            // MARK: - section_one
        
            internal func logScreenS1DefOne() {
                logScreen(
                    name: "s1 def one",
                    path: "s1_def_one/",
                    params: nil
                )
            }
        
            internal func logEventS1DefTwo() {
                logEvent(
                    name: "s1 def two",
                    action: "test",
                    category: "test",
                    params: nil
                )
            }
        
            // MARK: - section_two
        
            internal func logScreenS2DefOne() {
                logScreen(
                    name: "s2 def one",
                    path: "s2_def_one/",
                    params: nil
                )
            }
        }

        """

        if extensionContent != expect {
            print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
        }
        XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
    }

    func testGeneratedExtensionContentMatomoAndFirebase() {
        // Given
        let sectionOne = AnalyticsCategory(id: "section_one")
        sectionOne.definitions = [
            getAnalyticsDefinition(id: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: .screen, tags: ["ios", "iosonly"]),
            getAnalyticsDefinition(id: "s1_def_two", action: "test", category: "test", name: "s1 def two", type: .event, tags: ["ios", "iosonly"]),
        ]

        let sectionTwo = AnalyticsCategory(id: "section_two")
        sectionTwo.definitions = [
            getAnalyticsDefinition(id: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: .screen, tags: ["ios","iosonly"]),
            getAnalyticsDefinition(id: "s2_def_two", action: "test", category: "test", name: "s2 def two", type: .event, tags: ["droid","droidonly"]),
        ]

        let sectionThree = AnalyticsCategory(id: "section_three")
        sectionThree.definitions = [
            getAnalyticsDefinition(id: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: .screen, tags: ["droid","droidonly"]),
            getAnalyticsDefinition(id: "s3_def_two", action: "test", category: "test", name: "s3 def two", type: .event, tags: ["droid","droidonly"]),
        ]

        // When
        let extensionContent = AnalyticsGenerator.getExtensionContent(
            targets: [TrackerType.matomo, TrackerType.firebase],
            sections: [sectionOne, sectionTwo, sectionThree],
            tags: ["ios", "iosonly"],
            isStatic: false,
            visibility: .internal
        )

        // Expect Analytics
        let expect = """
        // Generated by ResgenSwift.Analytics \(ResgenSwiftVersion)

        import Foundation
        import MatomoTracker
        import FirebaseAnalytics
        import os

        \(protocolString(visibility: .internal))

        \(matomoString())

        \(firebaseString())

        // MARK: - Traker Type

        internal enum TrackerType: CaseIterable {

            case matomo
            case firebase
        }

        // MARK: - Manager

        internal class AnalyticsManager {

            internal static var shared = AnalyticsManager()

            private init() {}

            // MARK: - Properties

            var managers: [TrackerType: AnalyticsManagerProtocol] = [:]
        
            private var isDebugMode: Bool = false

            private var isEnabled: Bool {
                if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
                    false
                } else {
                    true
                }
            }
        
            private let logger = Logger(subsystem: "resgen", category: "analytics")

            // MARK: - Enable Methods

            private func setAnalytics(enable: Bool, _ analytics: [TrackerType]) {
                managers.forEach { (key, value) in
                    if analytics.contains(where: { type in
                        type == key
                    }) {
                        value.setEnable(enable)
                    }
                }
            }

            internal func enableAnalytics(_ analytics: [TrackerType] = TrackerType.allCases) {
                setAnalytics(enable: true, analytics)
            }

            internal func disableAnalytics(_ analytics: [TrackerType] = TrackerType.allCases) {
                setAnalytics(enable: false, analytics)
            }

            internal func configure(siteId: String, url: String, isDebugMode: Bool = false) {
                self.isDebugMode = isDebugMode
                managers[TrackerType.matomo] = MatomoAnalyticsManager(
                    siteId: siteId,
                    url: url
                )
                managers[TrackerType.firebase] = FirebaseAnalyticsManager()
            }

            // MARK: - Private Log Methods
        
            private func formattedParams(_ params: [String: Any]?) -> String {
                guard let params = params, !params.isEmpty else { return "-" }
                
                let formattedParams = params.map { key, value in
                    "       \\(key): \\(value)"
                }
                .joined(separator: "\\n")
            
                return "\\n" + formattedParams
            }

            private func logScreen(
                name: String,
                path: String,
                params: [String: Any]?
            ) {
                guard isEnabled else {
                    if isDebugMode {
                        logger.log("Analytics disabled")
                    }
                    return 
                }
        
                managers.values.forEach { manager in
            
                    if isDebugMode {
                        logger.debug(\"""
                        🖥️ Screen:
                             Name: \\(name, privacy: .public)
                             Path: \\(path, privacy: .public)
                             Params: \\(self.formattedParams(params), privacy: .public)
                        \""")
                    }
        
                    manager.logScreen(
                        name: name,
                        path: path,
                        params: params
                    )
                }
            }
        
            private func logEvent(
                name: String,
                action: String,
                category: String,
                params: [String: Any]?
            ) {
                guard isEnabled else {
                    if isDebugMode {
                        logger.log("Analytics disabled")
                    }
                    return 
                }
        
                managers.values.forEach { manager in
            
                    if isDebugMode {
                        logger.debug(\"""
                        📊 Event:
                             Name: \\(name, privacy: .public)
                             Action: \\(action.isEmpty ? "-" : action, privacy: .public)
                             Category: \\(category.isEmpty ? "-" : category, privacy: .public)
                             Params: \\(self.formattedParams(params), privacy: .public)
                        \""")
                    }
        
                    manager.logEvent(
                        name: name,
                        action: action,
                        category: category,
                        params: params
                    )
                }
            }

            // MARK: - section_one

            internal func logScreenS1DefOne() {
                logScreen(
                    name: "s1 def one",
                    path: "s1_def_one/",
                    params: nil
                )
            }

            internal func logEventS1DefTwo() {
                logEvent(
                    name: "s1 def two",
                    action: "test",
                    category: "test",
                    params: nil
                )
            }

            // MARK: - section_two

            internal func logScreenS2DefOne() {
                logScreen(
                    name: "s2 def one",
                    path: "s2_def_one/",
                    params: nil
                )
            }
        }

        """

        if extensionContent != expect {
            print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
        }
        XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
    }
}

// CPD-ON
