$ErrorActionPreference = 'Stop';
Set-StrictMode -Version '2.0'

$here = Split-Path -Parent $MyInvocation.MyCommand.Path

Import-Module -Force "$here\..\..\modules\FnmsServices.psm1"

[System.Reflection.Assembly]::LoadWithPartialName("System.ServiceProcess") | Out-Null
$ServiceControllerStatus = [System.ServiceProcess.ServiceControllerStatus];


# this needs to mirror what the output of Get-BatchServiceState looks like
function New-FakeServiceState {
	param(
		[string]
		$ServiceName = 'BatchProcessor',
		$ServiceStatus = $ServiceControllerStatus::Stopped,
		$ServiceStatusSnapshot = $null,
		[scriptblock]
		$RefreshScript = {},
		[scriptblock]
		$StopScript = {},
		[scriptblock]
		$WaitForStatusScript = {}
	)

	New-Object PSObject -Property  @{
		Service = 
			New-Object PSObject -Property @{ 
				Name = $ServiceName;
				Status = $ServiceStatus;
			} | 
			Add-Member -PassThru -Name 'Refresh' -Value $RefreshScript -MemberType 'ScriptMethod' |
			Add-Member -PassThru -Name 'Stop' -Value $StopScript -MemberType 'ScriptMethod' |
			Add-Member -PassThru -Name 'WaitForStatus' -Value $WaitForStatusScript -MemberType 'ScriptMethod';
		StatusSnapshot = $ServiceStatusSnapshot;
	}
}

Describe 'When I capture service state' -Tags "services","Get-BatchServiceState" {	
	It 'should accept empty input and return empty array' {
		{ @() | Get-BatchServiceState } | Should Not Throw
		$s = @(@() | Get-BatchServiceState)
		$s.Count | Should Be 0
		{ Get-BatchServiceState } | Should Not Throw
		@(Get-BatchServiceState).Count | Should Be 0
	}
}	

Describe 'When I check the status of a service' -Tags 'services',"Assert-ServiceStatusForDeployment" {

	$serviceState = New-FakeServiceState 'BatchProcessor'

	It "throws on unexpected service status <State>" -TestCases @(
		@{ State = $ServiceControllerStatus::ContinuePending },
		@{ State = $ServiceControllerStatus::Paused },
		@{ State = $ServiceControllerStatus::PausePending },
		@{ State = $ServiceControllerStatus::StartPending },
		@{ State = $ServiceControllerStatus::StopPending }
	) {
		param($State)

		$serviceState.Service.Status = $State
		{ Assert-ServiceStatusForDeployment $serviceState } | Should Throw "The state of the service BatchProcessor was unexpected"
	}

	It "accepts good service status <State>"  -TestCases @(
		@{ State = $ServiceControllerStatus::Stopped },
		@{ State = $ServiceControllerStatus::Running }
	) {
		param($State)

		$serviceState.Service.Status = $State
		{ Assert-ServiceStatusForDeployment $serviceState } | Should Not Throw
	}

	It 'should accept empty input and return empty array' {
		{ @() | Assert-ServiceStatusForDeployment } | Should Not Throw
		@(@() | Assert-ServiceStatusForDeployment).Count | Should Be 0
	}
}	

Describe 'When I attempt to stop running services' -Tags 'services',"Enter-DeploymentMode" {
	$mockBatchProcessorService = New-FakeServiceState 'BatchProcessor' $ServiceControllerStatus::Running

	Mock Assert-ServiceStatusForDeployment -ModuleName FnmsServices -MockWith { Write-Output @($mockBatchProcessorService) }.GetNewClosure();
	Mock Stop-BatchProcessor -ModuleName FnmsServices  -Scope It -MockWith { }		

	It 'should do service status check' {		
		Enter-DeploymentMode $mockBatchProcessorService
		Assert-MockCalled Assert-ServiceStatusForDeployment -ModuleName FnmsServices  -Scope It
	}

	It 'should call Stop-BatchProcessor to stop Batch Processor service' {
		Enter-DeploymentMode $mockBatchProcessorService
		Assert-MockCalled Stop-BatchProcessor -ModuleName FnmsServices  -Scope It
	}
}

Describe 'When I attempt to stop the batch processor' -Tags 'services',"Enter-DeploymentMode" {
	$mockBatchProcessorService = New-FakeServiceState 'BatchProcessor' $ServiceControllerStatus::Running
	Mock Set-ProcessDispatchState -ModuleName FnmsServices  -Scope It -MockWith { }		
	Mock Complete-ProcessorTasks -ModuleName FnmsServices  -Scope It -MockWith { }		
	Mock Stop-ServiceSync -ModuleName FnmsServices  -Scope It -MockWith { Write-Host $service.Service.Name}		

# Set-ProcessDispatchState
# Complete-ProcessorTasks
# Stop-ServiceSync

	It 'should stop the batchprocessor service' {
		Stop-BatchProcessor -Service $mockBatchProcessorService
		Assert-MockCalled Stop-ServiceSync -ModuleName FnmsServices  -Scope It
	}

	It 'should call Stop-BatchProcessor to stop Batch Processor service' {
	}
}

Describe 'When I attempt to stop multiple running services' -Tags 'services',"Enter-DeploymentMode" {
	$mockBatchProcessorService = New-FakeServiceState 'BatchProcessor' $ServiceControllerStatus::Running
	$mockBatchProcessSchedulerService = New-FakeServiceState 'BatchProcessScheduler' $ServiceControllerStatus::Running
	$serviceStopOrder = New-Object 'System.Collections.ArrayList'
	
	Mock Stop-BatchProcessor -ModuleName FnmsServices `
		-ParameterFilter { $Service.Service.Name -eq 'BatchProcessor' } `
		-MockWith { 
			param($Service)

			$serviceStopOrder.Add(1)
		}.GetNewClosure();

	Mock Stop-ServiceSync -ModuleName FnmsServices `
		-ParameterFilter { $Service.Service.Name -eq 'BatchProcessScheduler' } `
		-MockWith {
			param($Service)
			$serviceStopOrder.Add(2)
		}.GetNewClosure();

	Mock Stop-ServiceSync -ModuleName FnmsServices -MockWith{}

	BeforeEach {
		$serviceStopOrder.Clear();
	}

	It 'should stop Batch Processor service before others' {
		
		$mockBatchProcessorService, $mockBatchProcessSchedulerService | Enter-DeploymentMode
		
		Assert-MockCalled Stop-BatchProcessor -ModuleName FnmsServices -Scope It -Times 1 -ParameterFilter { $Service.Service.Name -eq 'BatchProcessor' }
		Assert-MockCalled Stop-ServiceSync -ModuleName FnmsServices -Scope It -Times 1 -ParameterFilter { $Service.Service.Name -eq 'BatchProcessScheduler' }

		$serviceStopOrder[0] | should be 1
		$serviceStopOrder[1] | should be 2
	}	

	It 'should snapshot current status for both running service and stopped service' {
		$mockBatchProcessSchedulerService.Service.Status = $ServiceControllerStatus::Running
		Enter-DeploymentMode $mockBatchProcessSchedulerService
		$mockBatchProcessSchedulerService.StatusSnapshot | Should be Running
		
		$mockBatchProcessSchedulerService.Service.Status = $ServiceControllerStatus::Stopped
		Enter-DeploymentMode $mockBatchProcessSchedulerService
		$mockBatchProcessSchedulerService.StatusSnapshot | Should be Stopped

		$mockBatchProcessSchedulerService.Service.Status = $ServiceControllerStatus::Stopped
		$mockBatchProcessorService.Service.Status = $ServiceControllerStatus::Running
		$mockBatchProcessorService, $mockBatchProcessSchedulerService | Enter-DeploymentMode
		$mockBatchProcessSchedulerService.StatusSnapshot | Should be Stopped
		$mockBatchProcessorService.StatusSnapshot | Should be Running
	}

	It 'should accept empty input and return empty array' {
		{ @() | Enter-DeploymentMode } | Should Not Throw
		@(@() | Enter-DeploymentMode).Count | Should Be 0
		# { Enter-DeploymentMode } | Should Not Throw
		# @(Enter-DeploymentMode).Count | Should Be 0
	}
}

Describe 'When I restart services after deployment' -Tags 'services', "Exit-DeploymentMode" {
	$mockBatchProcessorService = New-FakeServiceState 'BatchProcessor' $ServiceControllerStatus::Running
	$mockBatchProcessSchedulerService = New-FakeServiceState 'BatchProcessScheduler' $ServiceControllerStatus::Running
	
	Mock Start-BatchProcessor -ModuleName FnmsServices -MockWith { }
	Mock Start-Service -ModuleName FnmsServices -Scope It  -MockWith { }

	It 'should start service if service was previously running' {
		$mockBatchProcessorService.StatusSnapshot = $ServiceControllerStatus::Running
		$mockBatchProcessorService | Exit-DeploymentMode 
		Assert-MockCalled Start-BatchProcessor -ModuleName FnmsServices -Scope It 

		$mockBatchProcessSchedulerService.StatusSnapshot = $ServiceControllerStatus::Running
		$mockBatchProcessSchedulerService | Exit-DeploymentMode 
		Assert-MockCalled Start-Service -ModuleName FnmsServices -Scope It

		$mockBatchProcessorService, $mockBatchProcessSchedulerService | Exit-DeploymentMode 
		Assert-MockCalled Start-BatchProcessor -ModuleName FnmsServices -Scope It 2
		Assert-MockCalled Start-Service -ModuleName FnmsServices -Scope It 2
	}

	It 'should not start service if service was previously stopped' {
		$mockBatchProcessorService.StatusSnapshot = $ServiceControllerStatus::Stopped
		Exit-DeploymentMode $mockBatchProcessorService
		Assert-MockCalled Start-BatchProcessor -ModuleName FnmsServices -Scope It 0
		
		$mockBatchProcessSchedulerService.StatusSnapshot = $ServiceControllerStatus::Stopped
		Exit-DeploymentMode $mockBatchProcessSchedulerService
		Assert-MockCalled Start-Service -ModuleName FnmsServices -Scope It 0

		$mockBatchProcessorService, $mockBatchProcessSchedulerService | Exit-DeploymentMode
		Assert-MockCalled Start-BatchProcessor -ModuleName FnmsServices -Scope It 0
		Assert-MockCalled Start-Service -ModuleName FnmsServices -Scope It 0
	}

	It 'should accept empty input' {
		{ @() | Exit-DeploymentMode } | Should Not Throw
		@(@() | Exit-DeploymentMode).Count | Should Be 0
	}
}

Describe 'When I stop a service synchronously' -Tags 'Stop-ServiceSync' {

	Mock Assert-ServiceStatusForDeployment -ModuleName 'FnmsServices' -MockWith {};
	Mock Start-Sleep { Write-Host 'sleeping' } ;

	It 'checks the state of the service'{
		$service = New-FakeServiceState 'BatchProcessor'

		Stop-ServiceSync $service

		Assert-MockCalled Assert-ServiceStatusForDeployment -ModuleName 'FnmsServices' -Times 1
	}

	It 'stops all services on the pipeline' {
		$script:pipelineCalls = 0

		$services = @( 
			(New-FakeServiceState 'one' $ServiceControllerStatus::Running -StopScript { $script:pipelineCalls += 1}),
			(New-FakeServiceState 'two' $ServiceControllerStatus::Running -StopScript { $script:pipelineCalls += 1})
		)

		{ $services | Stop-ServiceSync } | Should Not Throw

		$script:pipelineCalls | Should Be 2
	}

	It 'stops services by parameter' {
		$script:pipelineCalls = 0

		$service = New-FakeServiceState 'one' $ServiceControllerStatus::Running -StopScript { $script:pipelineCalls += 1 }

		{ Stop-ServiceSync $service } | Should Not Throw

		$script:pipelineCalls | Should Be 1
	}

	It 'does not output to the pipeline by default' {
		$services = @( 
			(New-FakeServiceState 'one' $ServiceControllerStatus::Running),
			(New-FakeServiceState 'two' $ServiceControllerStatus::Running)
		)

		@($services | Stop-ServiceSync).Length | Should Be 0
	}

	It 'passes the service through on the pipeline if requested' {
		$services = @( 
			(New-FakeServiceState 'one' $ServiceControllerStatus::Running),
			(New-FakeServiceState 'two' $ServiceControllerStatus::Running)
		)

		@($services | Stop-ServiceSync -PassThru).Length | Should Be 2
	}

	It 'ignores a stopped service' {
		$script:pipelineCalls = 0

		$services = @( 
			(New-FakeServiceState 'one' $ServiceControllerStatus::Stopped -StopScript { $script:pipelineCalls += 1}),
			(New-FakeServiceState 'two' $ServiceControllerStatus::Stopped -StopScript { $script:pipelineCalls += 1})
		)

		$services | Stop-ServiceSync

		$script:pipelineCalls | Should Be 0
	}

	It 'waits for the service to stop' {
		$script:waitCalls = 0

		$services = @( 
			(New-FakeServiceState 'one' $ServiceControllerStatus::Running -StopScript { $script:waitCalls += 1 }),
			(New-FakeServiceState 'two' $ServiceControllerStatus::Running -StopScript { $script:waitCalls += 1 })
		)

		$services | Stop-ServiceSync

		$script:waitCalls | Should Be 2
	}
}