// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.

import Combine
import GRDB
import Quick
import Nimble
import SessionUIKit
import SessionSnodeKit
import SessionUtilitiesKit

@testable import Session

class ThreadSettingsViewModelSpec: QuickSpec {
    override class func spec() {
        // MARK: Configuration
        
        @TestState var mockStorage: Storage! = SynchronousStorage(
            customWriter: try! DatabaseQueue(),
            customMigrationTargets: [
                SNUtilitiesKit.self,
                SNSnodeKit.self,
                SNMessagingKit.self,
                SNUIKit.self
            ],
            initialData: { db in
                try Identity(
                    variant: .x25519PublicKey,
                    data: Data(hex: TestConstants.publicKey)
                ).insert(db)
                
                try SessionThread(id: "TestId",variant: .contact).insert(db)
                try Profile(id: "05\(TestConstants.publicKey)", name: "TestMe").insert(db)
                try Profile(id: "TestId", name: "TestUser").insert(db)
            }
        )
        @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache(
            initialSetup: { cache in
                cache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)")
            }
        )
        @TestState var mockCaches: MockCaches! = MockCaches()
            .setting(cache: .general, to: mockGeneralCache)
        @TestState var dependencies: Dependencies! = Dependencies(
            storage: mockStorage,
            caches: mockCaches,
            scheduler: .immediate
        )
        @TestState var threadVariant: SessionThread.Variant! = .contact
        @TestState var didTriggerSearchCallbackTriggered: Bool! = false
        @TestState var viewModel: ThreadSettingsViewModel! = ThreadSettingsViewModel(
            threadId: "TestId",
            threadVariant: .contact,
            didTriggerSearch: {
                didTriggerSearchCallbackTriggered = true
            },
            using: dependencies
        )
        
        @TestState var disposables: [AnyCancellable]! = [
            viewModel.observableTableData
                .receive(on: ImmediateScheduler.shared)
                .sink(
                    receiveCompletion: { _ in },
                    receiveValue: { viewModel.updateTableData($0.0) }
                )
        ]
        
        // MARK: - a ThreadSettingsViewModel
        describe("a ThreadSettingsViewModel") {
            // MARK: -- with any conversation type
            context("with any conversation type") {
                // MARK: ---- triggers the search callback when tapping search
                it("triggers the search callback when tapping search") {
                    viewModel.tableData
                        .first(where: { $0.model == .content })?
                        .elements
                        .first(where: { $0.id == .searchConversation })?
                        .onTap?()
                    
                    expect(didTriggerSearchCallbackTriggered).to(beTrue())
                }
                
                // MARK: ---- mutes a conversation
                it("mutes a conversation") {
                    viewModel.tableData
                        .first(where: { $0.model == .content })?
                        .elements
                        .first(where: { $0.id == .notificationMute })?
                        .onTap?()
                    
                    expect(
                        mockStorage
                            .read { db in try SessionThread.fetchOne(db, id: "TestId") }?
                            .mutedUntilTimestamp
                    )
                    .toNot(beNil())
                }
                
                // MARK: ---- unmutes a conversation
                it("unmutes a conversation") {
                    mockStorage.write { db in
                        try SessionThread
                            .updateAll(
                                db,
                                SessionThread.Columns.mutedUntilTimestamp.set(to: 1234567890)
                            )
                    }
                    
                    expect(
                        mockStorage
                            .read { db in try SessionThread.fetchOne(db, id: "TestId") }?
                            .mutedUntilTimestamp
                    )
                    .toNot(beNil())
                    
                    viewModel.tableData
                        .first(where: { $0.model == .content })?
                        .elements
                        .first(where: { $0.id == .notificationMute })?
                        .onTap?()
                
                    expect(
                        mockStorage
                            .read { db in try SessionThread.fetchOne(db, id: "TestId") }?
                            .mutedUntilTimestamp
                    )
                    .to(beNil())
                }
            }
            
            // MARK: -- with a note-to-self conversation
            context("with a note-to-self conversation") {
                beforeEach {
                    mockStorage.write { db in
                        try SessionThread.deleteAll(db)
                        
                        try SessionThread(
                            id: "05\(TestConstants.publicKey)",
                            variant: .contact
                        ).insert(db)
                    }
                    
                    viewModel = ThreadSettingsViewModel(
                        threadId: "05\(TestConstants.publicKey)",
                        threadVariant: .contact,
                        didTriggerSearch: {
                            didTriggerSearchCallbackTriggered = true
                        },
                        using: dependencies
                    )
                    disposables.append(
                        viewModel.observableTableData
                            .receive(on: ImmediateScheduler.shared)
                            .sink(
                                receiveCompletion: { _ in },
                                receiveValue: { viewModel.updateTableData($0.0) }
                            )
                    )
                }
                
                // MARK: ---- has the correct title
                it("has the correct title") {
                    expect(viewModel.title).to(equal("vc_settings_title".localized()))
                }
                
                // MARK: ---- starts in the standard nav state
                it("starts in the standard nav state") {
                    expect(viewModel.navState.firstValue())
                        .to(equal(.standard))
                    
                    expect(viewModel.leftNavItems.firstValue()).to(equal([]))
                    expect(viewModel.rightNavItems.firstValue())
                        .to(equal([
                            ParentType.NavItem(
                                id: .edit,
                                systemItem: .edit,
                                accessibilityIdentifier: "Edit button"
                            )
                        ]))
                }
                
                // MARK: ---- has no mute button
                it("has no mute button") {
                    expect(
                        viewModel.tableData
                            .first(where: { $0.model == .content })?
                            .elements
                            .first(where: { $0.id == .notificationMute })
                    ).to(beNil())
                }
                
                // MARK: ---- when entering edit mode
                context("when entering edit mode") {
                    beforeEach {
                        viewModel.navState.sinkAndStore(in: &disposables)
                        viewModel.rightNavItems.firstValue()??.first?.action?()
                        viewModel.textChanged("TestNew", for: .nickname)
                    }
                    
                    // MARK: ------ enters the editing state
                    it("enters the editing state") {
                        expect(viewModel.navState.firstValue())
                            .to(equal(.editing))
                        
                        expect(viewModel.leftNavItems.firstValue())
                            .to(equal([
                                ParentType.NavItem(
                                    id: .cancel,
                                    systemItem: .cancel,
                                    accessibilityIdentifier: "Cancel button"
                                )
                            ]))
                        expect(viewModel.rightNavItems.firstValue())
                            .to(equal([
                                ParentType.NavItem(
                                    id: .done,
                                    systemItem: .done,
                                    accessibilityIdentifier: "Done"
                                )
                            ]))
                    }
                    
                    // MARK: ------ when cancelling edit mode
                    context("when cancelling edit mode") {
                        beforeEach {
                            viewModel.leftNavItems.firstValue()??.first?.action?()
                        }
                        
                        // MARK: -------- exits editing mode
                        it("exits editing mode") {
                            expect(viewModel.navState.firstValue())
                                .to(equal(.standard))
                            
                            expect(viewModel.leftNavItems.firstValue()).to(equal([]))
                            expect(viewModel.rightNavItems.firstValue())
                                .to(equal([
                                    ParentType.NavItem(
                                        id: .edit,
                                        systemItem: .edit,
                                        accessibilityIdentifier: "Edit button"
                                    )
                                ]))
                        }
                        
                        // MARK: -------- does not update the nickname for the current user
                        it("does not update the nickname for the current user") {
                            expect(
                                mockStorage
                                    .read { db in
                                        try Profile.fetchOne(db, id: "05\(TestConstants.publicKey)")
                                    }?
                                    .nickname
                            )
                            .to(beNil())
                        }
                    }
                    
                    // MARK: ------ when saving edit mode
                    context("when saving edit mode") {
                        beforeEach {
                            viewModel.rightNavItems.firstValue()??.first?.action?()
                        }
                        
                        // MARK: -------- exits editing mode
                        it("exits editing mode") {
                            expect(viewModel.navState.firstValue())
                                .to(equal(.standard))
                            
                            expect(viewModel.leftNavItems.firstValue()).to(equal([]))
                            expect(viewModel.rightNavItems.firstValue())
                                .to(equal([
                                    ParentType.NavItem(
                                        id: .edit,
                                        systemItem: .edit,
                                        accessibilityIdentifier: "Edit button"
                                    )
                                ]))
                        }
                        
                        // MARK: -------- updates the nickname for the current user
                        it("updates the nickname for the current user") {
                            expect(
                                mockStorage
                                    .read { db in
                                        try Profile.fetchOne(db, id: "05\(TestConstants.publicKey)")
                                    }?
                                    .nickname
                            )
                            .to(equal("TestNew"))
                        }
                    }
                }
            }
            
            // MARK: -- with a one-to-one conversation
            context("with a one-to-one conversation") {
                beforeEach {
                    mockStorage.write { db in
                        try SessionThread.deleteAll(db)
                        
                        try SessionThread(
                            id: "TestId",
                            variant: .contact
                        ).insert(db)
                    }
                }
                
                // MARK: ---- has the correct title
                it("has the correct title") {
                    expect(viewModel.title).to(equal("vc_settings_title".localized()))
                }
                
                // MARK: ---- starts in the standard nav state
                it("starts in the standard nav state") {
                    expect(viewModel.navState.firstValue())
                        .to(equal(.standard))
                    
                    expect(viewModel.leftNavItems.firstValue()).to(equal([]))
                    expect(viewModel.rightNavItems.firstValue())
                        .to(equal([
                            ParentType.NavItem(
                                id: .edit,
                                systemItem: .edit,
                                accessibilityIdentifier: "Edit button"
                            )
                        ]))
                }
                
                // MARK: ---- when entering edit mode
                context("when entering edit mode") {
                    beforeEach {
                        viewModel.navState.sinkAndStore(in: &disposables)
                        viewModel.rightNavItems.firstValue()??.first?.action?()
                        viewModel.textChanged("TestUserNew", for: .nickname)
                    }
                    
                    // MARK: ------ enters the editing state
                    it("enters the editing state") {
                        expect(viewModel.navState.firstValue())
                            .to(equal(.editing))
                        
                        expect(viewModel.leftNavItems.firstValue())
                            .to(equal([
                                ParentType.NavItem(
                                    id: .cancel,
                                    systemItem: .cancel,
                                    accessibilityIdentifier: "Cancel button"
                                )
                            ]))
                        expect(viewModel.rightNavItems.firstValue())
                            .to(equal([
                                ParentType.NavItem(
                                    id: .done,
                                    systemItem: .done,
                                    accessibilityIdentifier: "Done"
                                )
                            ]))
                    }
                    
                    // MARK: ------ when cancelling edit mode
                    context("when cancelling edit mode") {
                        beforeEach {
                            viewModel.leftNavItems.firstValue()??.first?.action?()
                        }
                        
                        // MARK: -------- exits editing mode
                        it("exits editing mode") {
                            expect(viewModel.navState.firstValue())
                                .to(equal(.standard))
                            
                            expect(viewModel.leftNavItems.firstValue()).to(equal([]))
                            expect(viewModel.rightNavItems.firstValue())
                                .to(equal([
                                    ParentType.NavItem(
                                        id: .edit,
                                        systemItem: .edit,
                                        accessibilityIdentifier: "Edit button"
                                    )
                                ]))
                        }
                        
                        // MARK: -------- does not update the nickname for the current user
                        it("does not update the nickname for the current user") {
                            expect(
                                mockStorage
                                    .read { db in try Profile.fetchOne(db, id: "TestId") }?
                                    .nickname
                            )
                            .to(beNil())
                        }
                    }
                    
                    // MARK: ------ when saving edit mode
                    context("when saving edit mode") {
                        beforeEach {
                            viewModel.rightNavItems.firstValue()??.first?.action?()
                        }
                        
                        // MARK: -------- exits editing mode
                        it("exits editing mode") {
                            expect(viewModel.navState.firstValue())
                                .to(equal(.standard))
                            
                            expect(viewModel.leftNavItems.firstValue()).to(equal([]))
                            expect(viewModel.rightNavItems.firstValue())
                                .to(equal([
                                    ParentType.NavItem(
                                        id: .edit,
                                        systemItem: .edit,
                                        accessibilityIdentifier: "Edit button"
                                    )
                                ]))
                        }
                        
                        // MARK: -------- updates the nickname for the current user
                        it("updates the nickname for the current user") {
                            expect(
                                mockStorage
                                    .read { db in try Profile.fetchOne(db, id: "TestId") }?
                                    .nickname
                            )
                            .to(equal("TestUserNew"))
                        }
                    }
                }
            }
            
            // MARK: -- with a group conversation
            context("with a group conversation") {
                beforeEach {
                    mockStorage.write { db in
                        try SessionThread.deleteAll(db)
                        
                        try SessionThread(
                            id: "TestId",
                            variant: .legacyGroup
                        ).insert(db)
                    }
                    
                    viewModel = ThreadSettingsViewModel(
                        threadId: "TestId",
                        threadVariant: .legacyGroup,
                        didTriggerSearch: {
                            didTriggerSearchCallbackTriggered = true
                        },
                        using: dependencies
                    )
                    disposables.append(
                        viewModel.observableTableData
                            .receive(on: ImmediateScheduler.shared)
                            .sink(
                                receiveCompletion: { _ in },
                                receiveValue: { viewModel.updateTableData($0.0) }
                            )
                    )
                }
                
                // MARK: ---- has the correct title
                it("has the correct title") {
                    expect(viewModel.title).to(equal("vc_group_settings_title".localized()))
                }
                
                // MARK: ---- starts in the standard nav state
                it("starts in the standard nav state") {
                    expect(viewModel.navState.firstValue())
                        .to(equal(.standard))
                    
                    expect(viewModel.leftNavItems.firstValue()).to(equal([]))
                    expect(viewModel.rightNavItems.firstValue()).to(equal([]))
                }
            }
            
            // MARK: -- with a community conversation
            context("with a community conversation") {
                beforeEach {
                    mockStorage.write { db in
                        try SessionThread.deleteAll(db)
                        
                        try SessionThread(
                            id: "TestId",
                            variant: .community
                        ).insert(db)
                    }
                    
                    viewModel = ThreadSettingsViewModel(
                        threadId: "TestId",
                        threadVariant: .community,
                        didTriggerSearch: {
                            didTriggerSearchCallbackTriggered = true
                        },
                        using: dependencies
                    )
                    disposables.append(
                        viewModel.observableTableData
                            .receive(on: ImmediateScheduler.shared)
                            .sink(
                                receiveCompletion: { _ in },
                                receiveValue: { viewModel.updateTableData($0.0) }
                            )
                    )
                }
                
                // MARK: ---- has the correct title
                it("has the correct title") {
                    expect(viewModel.title).to(equal("vc_group_settings_title".localized()))
                }
                
                // MARK: ---- starts in the standard nav state
                it("starts in the standard nav state") {
                    expect(viewModel.navState.firstValue())
                        .to(equal(.standard))
                    
                    expect(viewModel.leftNavItems.firstValue()).to(equal([]))
                    expect(viewModel.rightNavItems.firstValue()).to(equal([]))
                }
            }
        }
    }
}

// MARK: - Test Types

fileprivate typealias ParentType = SessionTableViewModel<ThreadSettingsViewModel.NavButton, ThreadSettingsViewModel.Section, ThreadSettingsViewModel.Setting>