//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import XCTest

@testable import Element

class RegistrationTests: XCTestCase {
    /// Makes an authentication session that mimics the matrix.org flow.
    func makeSession() -> MXAuthenticationSession {
        let flow = MXLoginFlow()
        flow.stages = [kMXLoginFlowTypeRecaptcha, kMXLoginFlowTypeTerms, kMXLoginFlowTypeEmailIdentity]
        
        let session = MXAuthenticationSession()
        session.flows = [flow]
        session.params = [:]
        return session
    }
    
    /// Makes an authentication session that has two flows.
    func makeSessionWithTwoFlows() -> MXAuthenticationSession {
        let flow1 = MXLoginFlow()
        flow1.stages = [kMXLoginFlowTypeMSISDN, kMXLoginFlowTypeTerms, kMXLoginFlowTypeRecaptcha]
        
        let flow2 = MXLoginFlow()
        flow2.stages = [kMXLoginFlowTypeEmailIdentity, kMXLoginFlowTypeTerms, kMXLoginFlowTypeRecaptcha]
        
        let session = MXAuthenticationSession()
        session.flows = [flow1, flow2]
        session.params = [:]
        return session
    }
    
    func testRegistrationResultForNewSession() {
        // Given a fresh session.
        let session = makeSession()
        
        // Then the result should have no completed stages.
        let flowResult = session.flowResult
        XCTAssertEqual(flowResult.completedStages.count, 0,
                       "There should be no completed stages for a new session.")
        XCTAssertEqual(flowResult.missingStages.count, 3,
                       "The result should have 3 missing stages.")
        XCTAssertEqual(flowResult.nextUncompletedStage, .reCaptcha(isMandatory: true, siteKey: ""),
                       "The first stage should match the order in the session.")
        XCTAssertEqual(flowResult.nextUncompletedStageOrdered, .email(isMandatory: true),
                       "The first stage when ordered should be Email for a new session.")
        XCTAssertFalse(flowResult.needsFallback,
                       "Fallback shouldn't be needed when the stages are all supported.")
    }
    
    func testRegistrationResultAfterEmail() {
        // Given a fresh session.
        let session = makeSession()
        
        // When completing the email stage.
        session.completed = [kMXLoginFlowTypeEmailIdentity]
        
        // Then the result should reflect the first stage has been completed.
        let flowResult = session.flowResult
        XCTAssertEqual(flowResult.completedStages.count, 1,
                       "The result should have 1 completed stage.")
        XCTAssertEqual(flowResult.missingStages.count, 2,
                       "The result should have 2 missing stages.")
        XCTAssertEqual(flowResult.nextUncompletedStage, .reCaptcha(isMandatory: true, siteKey: ""),
                       "The next stage should be the ReCaptcha stage.")
        XCTAssertEqual(flowResult.nextUncompletedStageOrdered, .terms(isMandatory: true, terms: MXLoginTerms(fromJSON: [:])),
                       "The next stage when ordered should be the Terms stage.")
    }
    
    func testRegistrationResultAfterEmailAndTerms() {
        // Given a fresh session.
        let session = makeSession()
        
        // When completing the email and terms stages.
        session.completed = [kMXLoginFlowTypeEmailIdentity, kMXLoginFlowTypeTerms]
        
        // Then the result should reflect the first 2 stages have been completed.
        let flowResult = session.flowResult
        XCTAssertEqual(flowResult.completedStages.count, 2,
                       "The result should have 2 completed stages.")
        XCTAssertEqual(flowResult.missingStages.count, 1,
                       "The result should have 1 missing stage.")
        XCTAssertEqual(flowResult.nextUncompletedStage, .reCaptcha(isMandatory: true, siteKey: ""),
                       "The next stage should be the ReCaptcha stage.")
        XCTAssertEqual(flowResult.nextUncompletedStageOrdered, .reCaptcha(isMandatory: true, siteKey: ""),
                       "The next stage when ordered should be the ReCaptcha stage.")
    }
    
    func testRegistrationResultAfterAllStages() {
        // Given a fresh session.
        let session = makeSession()
        
        // When completing all of the stages.
        session.completed = [kMXLoginFlowTypeEmailIdentity, kMXLoginFlowTypeTerms, kMXLoginFlowTypeRecaptcha]
        
        // Then the result shouldn't have any missing stages.
        let flowResult = session.flowResult
        XCTAssertEqual(flowResult.completedStages.count, 3,
                       "The result should have all completed stages.")
        XCTAssertEqual(flowResult.missingStages.count, 0,
                       "The result should have no missing stages.")
        XCTAssertNil(flowResult.nextUncompletedStage,
                     "There shouldn't be any more stages to complete.")
        XCTAssertNil(flowResult.nextUncompletedStageOrdered,
                     "There shouldn't be any more stages to complete.")
    }
    
    func testRegistrationResultCustomStage() {
        // Given a session that contains a single flow with a custom stage.
        let session = makeSession()
        session.flows.first?.stages.append("test.flow")
        
        // Then the result should indicate that fallback authentication should be used.
        let flowResult = session.flowResult
        XCTAssertTrue(flowResult.needsFallback, "Fallback should be required when a custom stage is present.")
    }
    
    func testRegistrationResultTwoFlows() {
        // Given a session with two flows.
        let session = makeSessionWithTwoFlows()
        
        // Then the result should know the mandatory/optional stages and start with the mandatory stages unless ordered
        let flowResult = session.flowResult
        XCTAssertFalse(flowResult.needsFallback,
                       "Fallback shouldn't be needed when the stages are all supported.")
        XCTAssertEqual(flowResult.nextUncompletedStage, .terms(isMandatory: true, terms: MXLoginTerms(fromJSON: [:])),
                       "The first stage should be the Terms stage.")
        XCTAssertEqual(flowResult.nextUncompletedStageOrdered, .email(isMandatory: false),
                       "The first stage when ordered should be the Email stage.")
        
        flowResult.missingStages.forEach { stage in
            switch stage {
            case .email(let isMandatory):
                XCTAssertFalse(isMandatory, "The Email stage should be optional.")
            case .msisdn(let isMandatory):
                XCTAssertFalse(isMandatory, "The MSISDN stage should be optional.")
            case .terms(let isMandatory, _):
                XCTAssertTrue(isMandatory, "The Terms stage should be mandatory.")
            case .reCaptcha(let isMandatory, _):
                XCTAssertTrue(isMandatory, "The ReCaptcha stage should be mandatory.")
            default:
                XCTFail("There shouldn't be any other types of stage in the result.")
            }
        }
    }
    
    func testRegistrationResultTwoFlowsAfterMandatoryStages() {
        // Given a session with two flows.
        let session = makeSessionWithTwoFlows()
        
        // When completing the terms and recaptcha stages.
        session.completed = [kMXLoginFlowTypeTerms, kMXLoginFlowTypeRecaptcha]
        
        // Then the result should have the optional stages remaining.
        let flowResult = session.flowResult
        XCTAssertEqual(flowResult.completedStages.count, 2,
                       "The result should have 2 completed stages.")
        XCTAssertEqual(flowResult.missingStages.count, 2,
                       "The result should have 2 missing stage.")
        XCTAssertEqual(flowResult.nextUncompletedStage, .msisdn(isMandatory: false),
                       "The next stage should be the MSISDN stage.")
        XCTAssertEqual(flowResult.nextUncompletedStageOrdered, .email(isMandatory: false),
                       "The next stage when ordered should be the Email stage.")
    }
    
    func testRegistrationResultTwoFlowsCustomStage() {
        // Given a session with a custom stage in a second flow.
        let session = makeSession()
        let flow = MXLoginFlow()
        flow.stages = ["test.flow"]
        session.flows.append(flow)
        
        // Then the session shouldn't need fallback.
        let flowResult = session.flowResult
        XCTAssertFalse(flowResult.needsFallback, "Fallback shouldn't be required when a custom stage is optional.")
    }
}
