[CmdletBinding()]
param(
    [string]$BaseUrl = 'http://127.0.0.1/CCW',
    [string]$Username = 'admin',
    [string]$Password = 'admin123',
    [string]$RestrictedUsername = '',
    [string]$RestrictedPassword = '',
    [string[]]$RestrictedAllowedBranches = @()
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

function Get-FormState {
    param(
        [string]$Html,
        [string]$ActionContains
    )

    $forms = [regex]::Matches($Html, '<form\b[^>]*action="([^"]*)"[^>]*>(.*?)</form>', 'IgnoreCase, Singleline')
    foreach ($form in $forms) {
        $action = [System.Net.WebUtility]::HtmlDecode($form.Groups[1].Value)
        if (-not $action.Contains($ActionContains)) {
            continue
        }

        $state = @{}
        $hiddenInputs = [regex]::Matches($form.Groups[2].Value, '<input\b[^>]*type="hidden"[^>]*name="([^"]+)"[^>]*value="([^"]*)"', 'IgnoreCase')
        foreach ($input in $hiddenInputs) {
            $name = [System.Net.WebUtility]::HtmlDecode($input.Groups[1].Value)
            $value = [System.Net.WebUtility]::HtmlDecode($input.Groups[2].Value)
            $state[$name] = $value
        }

        return $state
    }

    throw "Form not found for action: $ActionContains"
}

function Get-PageTitle {
    param([string]$Html)

    $match = [regex]::Match($Html, '<title>\s*(.*?)\s*</title>', 'IgnoreCase, Singleline')
    if (-not $match.Success) {
        return ''
    }

    return [System.Net.WebUtility]::HtmlDecode($match.Groups[1].Value).Trim()
}

function Get-FlashText {
    param([string]$Html)

    $match = [regex]::Match($Html, '<div class="flash[^"]*">\s*(.*?)\s*</div>', 'IgnoreCase, Singleline')
    if (-not $match.Success) {
        return ''
    }

    $text = [regex]::Replace($match.Groups[1].Value, '<[^>]+>', '')
    return [System.Net.WebUtility]::HtmlDecode($text).Trim()
}

function Get-ResponseText {
    param($Value)

    if ($null -eq $Value) {
        return ''
    }

    if ($Value -is [byte[]]) {
        return [System.Text.Encoding]::UTF8.GetString($Value)
    }

    if ($Value -is [string]) {
        return $Value
    }

    if ($Value -is [System.Array]) {
        return [string]::Join("`n", @($Value | ForEach-Object { [string]$_ }))
    }

    return [string]$Value
}

function Get-SelectOptions {
    param(
        [string]$Html,
        [string]$Name
    )

    $selectPattern = '<select\b[^>]*name="' + [regex]::Escape($Name) + '"[^>]*>(.*?)</select>'
    $selectMatch = [regex]::Match($Html, $selectPattern, 'IgnoreCase, Singleline')
    if (-not $selectMatch.Success) {
        throw "Select not found: $Name"
    }

    $options = New-Object System.Collections.Generic.List[string]
    $optionMatches = [regex]::Matches($selectMatch.Groups[1].Value, '<option\b[^>]*value="([^"]*)"[^>]*>', 'IgnoreCase')
    foreach ($option in $optionMatches) {
        $value = [System.Net.WebUtility]::HtmlDecode($option.Groups[1].Value).Trim()
        if ($value -eq '') {
            continue
        }

        $options.Add($value) | Out-Null
    }

    return $options.ToArray()
}

function Get-QueueOrderIds {
    param([string]$Html)

    $ids = New-Object System.Collections.Generic.List[int]
    $matches = [regex]::Matches($Html, 'href="job_details\.php\?id=(\d+)"', 'IgnoreCase')
    foreach ($match in $matches) {
        $orderId = [int]$match.Groups[1].Value
        if (-not $ids.Contains($orderId)) {
            $ids.Add($orderId) | Out-Null
        }
    }

    return $ids.ToArray()
}

function Get-JobSummary {
    param([string]$Html)

    $summary = @{
        BillNumber = ''
        BranchName = ''
    }

    $identityMatch = [regex]::Match(
        $Html,
        '<div class="stack">\s*<span class="muted">\s*(?:เลขที่บิล|Bill\s*no\.)\s*</span>\s*<strong>([^<]+)</strong>(?:.*?</div>)\s*<div class="stack">\s*<span class="muted">\s*(?:สาขา|Branch)\s*</span>\s*<strong>([^<]+)</strong>',
        'IgnoreCase, Singleline'
    )
    if ($identityMatch.Success) {
        $billNumber = [System.Net.WebUtility]::HtmlDecode($identityMatch.Groups[1].Value).Trim()
        $branchName = [System.Net.WebUtility]::HtmlDecode($identityMatch.Groups[2].Value).Trim()

        if ($billNumber -match '^(?:[A-Z]{3}\d{6}/\d{4}|#\d+)$') {
            $summary['BillNumber'] = $billNumber
        }

        if (-not [string]::IsNullOrWhiteSpace($branchName) -and $branchName -ne '-') {
            $summary['BranchName'] = $branchName
        }
    }

    return $summary
}

function Invoke-FlowRequest {
    param(
        [Microsoft.PowerShell.Commands.WebRequestSession]$Session,
        [string]$Uri,
        [string]$Method = 'GET',
        [hashtable]$Body = @{}
    )

    $params = @{
        Uri = $Uri
        Method = $Method
        WebSession = $Session
        UseBasicParsing = $true
    }

    if ($Method -eq 'POST') {
        $params.Body = $Body
    }

    return Invoke-WebRequest @params
}

function Assert-True {
    param(
        [object]$Condition,
        [string]$Message
    )

    if (-not [bool]$Condition) {
        throw $Message
    }
}

function Login-Session {
    param(
        [Microsoft.PowerShell.Commands.WebRequestSession]$Session,
        [string]$BaseUrl,
        [string]$Username,
        [string]$Password
    )

    $loginPage = Invoke-FlowRequest -Session $Session -Uri "$BaseUrl/pages/login.php"
    $loginForm = Get-FormState -Html (Get-ResponseText -Value $loginPage.Content) -ActionContains '../process/login.php'
    $null = Invoke-FlowRequest -Session $Session -Uri "$BaseUrl/process/login.php" -Method 'POST' -Body @{
        csrf_token = $loginForm['csrf_token']
        username = $Username
        password = $Password
        display_mode = 'app'
    }
}

$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$restrictedSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$results = [ordered]@{}

$publicTargets = @(
    @{ Name = 'manifest'; Uri = "$BaseUrl/manifest.webmanifest"; Marker = '"name"' },
    @{ Name = 'offline'; Uri = "$BaseUrl/offline.html"; Marker = 'CCW' },
    @{ Name = 'service-worker'; Uri = "$BaseUrl/service-worker.js"; Marker = 'self.addEventListener' }
)

foreach ($target in $publicTargets) {
    $response = Invoke-WebRequest -Uri ([string]$target.Uri) -UseBasicParsing
    $contentText = Get-ResponseText -Value $response.Content
    Assert-True -Condition ($response.StatusCode -eq 200) -Message "$($target.Name) returned unexpected status: $($response.StatusCode)"
    Assert-True -Condition ([regex]::IsMatch($contentText, [regex]::Escape([string]$target.Marker))) -Message "$($target.Name) did not contain expected marker."
    $results["public_$($target.Name)"] = 'passed'
}

Login-Session -Session $session -BaseUrl $BaseUrl -Username $Username -Password $Password
$results['login'] = 'passed'

$pageTargets = @(
    'dashboard.php',
    'booking.php',
    'queue.php',
    'quick_service.php',
    'manage_system.php',
    'member.php?view=list',
    'sale_report.php'
)

$queueResponse = $null
$bookingResponse = $null
foreach ($pageTarget in $pageTargets) {
    $response = Invoke-FlowRequest -Session $session -Uri "$BaseUrl/pages/$pageTarget"
    Assert-True -Condition ($response.StatusCode -eq 200) -Message "$pageTarget returned unexpected status: $($response.StatusCode)"

    $responseUri = $response.BaseResponse.ResponseUri.AbsoluteUri
    $expectedFragment = ($pageTarget -split '\?')[0]
    Assert-True -Condition ($responseUri -match [regex]::Escape($expectedFragment)) -Message "$pageTarget redirected unexpectedly to $responseUri"

    $title = Get-PageTitle -Html (Get-ResponseText -Value $response.Content)
    Assert-True -Condition (-not [string]::IsNullOrWhiteSpace($title)) -Message "$pageTarget did not render a title."
    Assert-True -Condition ([regex]::IsMatch($title, 'CCW')) -Message "$pageTarget title did not contain CCW."

    if ($expectedFragment -eq 'queue.php') {
        $queueResponse = $response
    }
    if ($expectedFragment -eq 'booking.php') {
        $bookingResponse = $response
    }

    $results["page_$expectedFragment"] = $title
}

if ($null -ne $queueResponse) {
    $jobMatch = [regex]::Match($queueResponse.Content, 'href="job_details\.php\?id=(\d+)"', 'IgnoreCase')
    if ($jobMatch.Success) {
        $jobId = [int]$jobMatch.Groups[1].Value
        $jobResponse = Invoke-FlowRequest -Session $session -Uri "$BaseUrl/pages/job_details.php?id=$jobId"
        Assert-True -Condition ($jobResponse.StatusCode -eq 200) -Message "job_details.php?id=$jobId returned unexpected status: $($jobResponse.StatusCode)"
        $jobTitle = Get-PageTitle -Html (Get-ResponseText -Value $jobResponse.Content)
        Assert-True -Condition (-not [string]::IsNullOrWhiteSpace($jobTitle)) -Message "job_details.php?id=$jobId did not render a title."
        $results['job_details_probe'] = "passed:$jobId"
    } else {
        $results['job_details_probe'] = 'skipped:no-queue-item'
    }
}

if ([string]::IsNullOrWhiteSpace($RestrictedUsername) -or [string]::IsNullOrWhiteSpace($RestrictedPassword)) {
    $results['branch_scope_post_deploy'] = 'skipped:no-restricted-credentials'
} else {
    Login-Session -Session $restrictedSession -BaseUrl $BaseUrl -Username $RestrictedUsername -Password $RestrictedPassword

    $restrictedBookingResponse = Invoke-FlowRequest -Session $restrictedSession -Uri "$BaseUrl/pages/booking.php"
    $restrictedBookingHtml = Get-ResponseText -Value $restrictedBookingResponse.Content
    $restrictedBranchOptions = @(Get-SelectOptions -Html $restrictedBookingHtml -Name 'branch_name')
    Assert-True -Condition ($restrictedBranchOptions.Count -gt 0) -Message 'Restricted booking page did not expose any branch options.'
    $results['restricted_branch_options'] = ($restrictedBranchOptions -join ', ')
    $adminBranchOptions = @()
    if ($null -ne $bookingResponse) {
        $adminBranchOptions = @(Get-SelectOptions -Html (Get-ResponseText -Value $bookingResponse.Content) -Name 'branch_name')
    }

    if ($RestrictedAllowedBranches.Count -gt 0) {
        $expectedBranches = @($RestrictedAllowedBranches | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() } | Sort-Object -Unique)
        $actualBranches = @($restrictedBranchOptions | Sort-Object -Unique)
        Assert-True -Condition (($expectedBranches -join '|') -eq ($actualBranches -join '|')) -Message "Restricted branch options did not match expected set. Expected: $($expectedBranches -join ', ') / Actual: $($actualBranches -join ', ')"
    }

    $allowedBranches = @($restrictedBranchOptions | Sort-Object -Unique)
    $adminVisibleOrderIds = @()
    $restrictedVisibleOrderIds = @()
    $seenAdminOrderIds = @{}
    $seenRestrictedOrderIds = @{}
    $queueStatuses = @('waiting', 'washing', 'done')
    foreach ($queueStatus in $queueStatuses) {
        $adminQueueStatusResponse = Invoke-FlowRequest -Session $session -Uri "$BaseUrl/pages/queue.php?status=$queueStatus"
        $adminQueueHtml = Get-ResponseText -Value $adminQueueStatusResponse.Content
        $adminOrderIds = @(Get-QueueOrderIds -Html $adminQueueHtml)
        foreach ($orderId in $adminOrderIds) {
            $orderIdKey = [string]$orderId
            if ($seenAdminOrderIds.ContainsKey($orderIdKey)) {
                continue
            }

            $adminVisibleOrderIds += $orderId
            $seenAdminOrderIds[$orderIdKey] = $true
        }

        $restrictedQueueStatusResponse = Invoke-FlowRequest -Session $restrictedSession -Uri "$BaseUrl/pages/queue.php?status=$queueStatus"
        $restrictedQueueHtml = Get-ResponseText -Value $restrictedQueueStatusResponse.Content
        $restrictedOrderIds = @(Get-QueueOrderIds -Html $restrictedQueueHtml)
        foreach ($orderId in $restrictedOrderIds) {
            $orderIdKey = [string]$orderId
            if ($seenRestrictedOrderIds.ContainsKey($orderIdKey)) {
                continue
            }

            $restrictedVisibleOrderIds += $orderId
            $seenRestrictedOrderIds[$orderIdKey] = $true
        }
    }

    $results['branch_scope_admin_visible_count'] = [string]$adminVisibleOrderIds.Count
    $results['branch_scope_restricted_visible_count'] = [string]$restrictedVisibleOrderIds.Count

    $allowedProbe = @($restrictedVisibleOrderIds | Select-Object -First 1)
    if ($allowedProbe.Count -gt 0) {
        $allowedProbeOrderId = [int]$allowedProbe[0]
        $restrictedAllowedJobResponse = Invoke-FlowRequest -Session $restrictedSession -Uri "$BaseUrl/pages/job_details.php?id=$allowedProbeOrderId"
        Assert-True -Condition ($restrictedAllowedJobResponse.StatusCode -eq 200) -Message "Restricted allowed job probe returned unexpected status: $($restrictedAllowedJobResponse.StatusCode)"
        $restrictedAllowedUri = $restrictedAllowedJobResponse.BaseResponse.ResponseUri.AbsoluteUri
        Assert-True -Condition ($restrictedAllowedUri -match [regex]::Escape("job_details.php?id=$allowedProbeOrderId")) -Message "Restricted allowed job probe redirected unexpectedly to $restrictedAllowedUri"
        $results['branch_scope_allowed_probe'] = "passed:$allowedProbeOrderId"
    } else {
        $results['branch_scope_allowed_probe'] = 'skipped:no-allowed-order'
    }

    if ($adminBranchOptions.Count -gt 0 -and $allowedBranches.Count -ge $adminBranchOptions.Count) {
        $results['branch_scope_blocked_probe'] = 'skipped:no-restriction-detected'
    } else {
        $blockedProbe = @($adminVisibleOrderIds | Where-Object { $restrictedVisibleOrderIds -notcontains $_ } | Select-Object -First 1)
        if ($blockedProbe.Count -gt 0) {
            $blockedProbeOrderId = [int]$blockedProbe[0]
            $restrictedBlockedJobResponse = Invoke-FlowRequest -Session $restrictedSession -Uri "$BaseUrl/pages/job_details.php?id=$blockedProbeOrderId"
            $restrictedBlockedHtml = Get-ResponseText -Value $restrictedBlockedJobResponse.Content
            $restrictedBlockedFlash = Get-FlashText -Html $restrictedBlockedHtml
            $restrictedBlockedUri = $restrictedBlockedJobResponse.BaseResponse.ResponseUri.AbsoluteUri
            Assert-True -Condition ($restrictedBlockedUri -notmatch [regex]::Escape("job_details.php?id=$blockedProbeOrderId")) -Message "Restricted blocked job probe unexpectedly stayed on restricted order page. Order: $blockedProbeOrderId / Uri: $restrictedBlockedUri"
            Assert-True -Condition (-not [string]::IsNullOrWhiteSpace($restrictedBlockedFlash)) -Message 'Restricted blocked job probe did not expose an access flash.'
            $results['branch_scope_blocked_probe'] = "passed:$blockedProbeOrderId"
        } else {
            $results['branch_scope_blocked_probe'] = 'skipped:no-blocked-order'
        }
    }

    $results['branch_scope_post_deploy'] = 'passed'
}

$results.GetEnumerator() | ForEach-Object {
    "{0}: {1}" -f $_.Key, $_.Value
}
