[CmdletBinding()]
param(
    [string]$BaseUrl = 'http://127.0.0.1/CCW',
    [string]$Username = 'admin',
    [string]$Password = 'admin123',
    [switch]$KeepTestData
)

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

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-FormState {
    param(
        [string]$Html,
        [string]$ActionContains,
        [hashtable]$RequiredHidden = @{}
    )

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

        $matchesRequired = $true
        foreach ($key in $RequiredHidden.Keys) {
            $pattern = '<input\b[^>]*name="' + [regex]::Escape([string]$key) + '"[^>]*value="' + [regex]::Escape([string]$RequiredHidden[$key]) + '"[^>]*>'
            if (-not [regex]::IsMatch($body, $pattern, 'IgnoreCase')) {
                $matchesRequired = $false
                break
            }
        }

        if (-not $matchesRequired) {
            continue
        }

        $state = @{}
        $hiddenInputs = [regex]::Matches($body, '<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-FirstSelectValue {
    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"
    }

    $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 -ne '') {
            return $value
        }
    }

    throw "No non-empty option found for select: $Name"
}

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

    $match = [regex]::Match(
        $Html,
        'name="service_ids\[\]"[^>]*value="(\d+)"[^>]*data-price="([^"]+)"',
        'IgnoreCase'
    )
    if (-not $match.Success) {
        throw 'No quick service item found on page.'
    }

    $serviceId = $match.Groups[1].Value
    $priceText = [System.Net.WebUtility]::HtmlDecode($match.Groups[2].Value)
    $price = 0.0
    if (-not [double]::TryParse($priceText, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$price)) {
        throw "Service price is invalid: $priceText"
    }

    return @{
        ServiceId = $serviceId
        Price = $price
    }
}

function Get-OrderIdFromResponse {
    param($Response)

    $uri = $Response.BaseResponse.ResponseUri.AbsoluteUri
    $match = [regex]::Match($uri, '[?&]id=(\d+)')
    if (-not $match.Success) {
        throw "Order id not found in redirect URL: $uri"
    }

    return [int]$match.Groups[1].Value
}

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 Invoke-MultipartFlowRequest {
    param(
        [Microsoft.PowerShell.Commands.WebRequestSession]$Session,
        [string]$Uri,
        [hashtable]$Fields,
        [string]$FileFieldName,
        [string]$FilePath,
        [string]$FileContentType
    )

    $handler = New-Object System.Net.Http.HttpClientHandler
    $handler.AllowAutoRedirect = $true
    $handler.CookieContainer = $Session.Cookies
    $client = New-Object System.Net.Http.HttpClient($handler)

    try {
        $content = New-Object System.Net.Http.MultipartFormDataContent

        foreach ($entry in $Fields.GetEnumerator()) {
            $values = if ($entry.Value -is [System.Array] -and -not ($entry.Value -is [string])) { $entry.Value } else { @($entry.Value) }
            foreach ($value in $values) {
                $stringContent = New-Object System.Net.Http.StringContent([string]$value, [System.Text.Encoding]::UTF8)
                $null = $content.Add($stringContent, [string]$entry.Key)
            }
        }

        $fileBytes = [System.IO.File]::ReadAllBytes($FilePath)
        $fileContent = New-Object System.Net.Http.ByteArrayContent(, $fileBytes)
        $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse($FileContentType)
        $null = $content.Add($fileContent, $FileFieldName, [System.IO.Path]::GetFileName($FilePath))

        $response = $client.PostAsync($Uri, $content).GetAwaiter().GetResult()
        $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()

        return [pscustomobject]@{
            Content = $body
            StatusCode = [int]$response.StatusCode
            BaseResponse = [pscustomobject]@{
                ResponseUri = $response.RequestMessage.RequestUri
            }
            Headers = $response.Headers
        }
    } finally {
        $client.Dispose()
        $handler.Dispose()
    }
}

function Assert-NotBlank {
    param(
        [string]$Value,
        [string]$Label
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        throw "$Label flash was blank."
    }
}

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

    if (-not $Condition) {
        throw $Message
    }
}

function Assert-Contains {
    param(
        [string]$Html,
        [string]$Needle,
        [string]$Label
    )

    if ($Html -notmatch [regex]::Escape($Needle)) {
        throw "$Label did not contain expected text: $Needle"
    }
}

function New-TempPngFile {
    param([string]$Prefix)

    $path = Join-Path ([System.IO.Path]::GetTempPath()) ($Prefix + '-' + [guid]::NewGuid().ToString('N') + '.png')
    $bytes = [Convert]::FromBase64String('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN6kAAAAASUVORK5CYII=')
    [System.IO.File]::WriteAllBytes($path, $bytes)
    return $path
}

function Cancel-Order {
    param(
        [Microsoft.PowerShell.Commands.WebRequestSession]$Session,
        [string]$BaseUrl,
        [int]$OrderId,
        [string]$ReturnTo,
        [string]$Reason
    )

    if ($OrderId -le 0) {
        return ''
    }

    $cancelPage = Invoke-FlowRequest -Session $Session -Uri "$BaseUrl/pages/cancel_bill.php?id=$OrderId&return_to=$([uri]::EscapeDataString($ReturnTo))"
    $cancelForm = Get-FormState -Html $cancelPage.Content -ActionContains '../process/cancel_bill.php'
    $cancelResult = Invoke-FlowRequest -Session $Session -Uri "$BaseUrl/process/cancel_bill.php" -Method 'POST' -Body @{
        csrf_token = $cancelForm['csrf_token']
        submission_token = $cancelForm['submission_token']
        order_id = $OrderId
        cancellation_reason = $Reason
        return_to = $ReturnTo
    }

    return Get-FlashText -Html $cancelResult.Content
}

$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$stamp = Get-Date -Format 'yyyyMMddHHmmss'
$plate = 'QT' + $stamp.Substring($stamp.Length - 6)
$guestName = "QS Slip $stamp"
$results = [ordered]@{}
$orderId = 0
$slipPath = ''

try {
    $loginPage = Invoke-FlowRequest -Session $session -Uri "$BaseUrl/pages/login.php"
    $loginForm = Get-FormState -Html $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'
    }

    $quickPage = Invoke-FlowRequest -Session $session -Uri "$BaseUrl/pages/quick_service.php"
    $quickForm = Get-FormState -Html $quickPage.Content -ActionContains '../process/save_quick_service.php'
    $branchName = Get-FirstSelectValue -Html $quickPage.Content -Name 'branch_name'
    $serviceItem = Get-FirstQuickServiceItem -Html $quickPage.Content
    $serviceId = [string]$serviceItem['ServiceId']
    $servicePrice = [double]$serviceItem['Price']
    $billTotal = if ($servicePrice -gt 0) { [math]::Round($servicePrice, 2) } else { 100.0 }

    $slipPath = New-TempPngFile -Prefix 'ccw-quick-slip'

    $paymentResult = Invoke-MultipartFlowRequest -Session $session -Uri "$BaseUrl/process/save_quick_service.php" -Fields @{
        csrf_token = $quickForm['csrf_token']
        customer_mode = 'guest'
        customer_id = ''
        car_id = ''
        customer_name = $guestName
        phone = ''
        plate_number = $plate
        province = 'Bangkok'
        brand = 'Mazda'
        model = '3'
        color = 'White'
        branch_name = $branchName
        'service_ids[]' = @($serviceId)
        total_price = ('{0:F2}' -f $billTotal)
        payment_method = 'transfer'
        received_amount = ('{0:F2}' -f $billTotal)
        notes = 'smoke quick service transfer slip'
        submit_action = 'pay'
    } -FileFieldName 'slip_file' -FilePath $slipPath -FileContentType 'image/png'

    $results['payment_flash'] = Get-FlashText -Html $paymentResult.Content
    if ([string]::IsNullOrWhiteSpace([string]$results['payment_flash'])) {
        $results['payment_flash'] = '[blank-on-receipt]'
    }

    $receiptUri = $paymentResult.BaseResponse.ResponseUri.AbsoluteUri
    $results['receipt_uri'] = $receiptUri
    if ($receiptUri -notmatch 'quick_receipt\.php\?id=\d+') {
        throw "Quick Service transfer payment did not redirect to receipt page: $receiptUri"
    }

    $orderId = Get-OrderIdFromResponse -Response $paymentResult
    $results['receipt_order_id'] = $orderId
    $results['receipt_status'] = [int]$paymentResult.StatusCode
    Assert-Contains -Html $paymentResult.Content -Needle 'receipt-paper' -Label 'Receipt page'

    $slipMatch = [regex]::Match($paymentResult.Content, 'view_payment_slip\.php\?payment_id=(\d+)', 'IgnoreCase')
    Assert-True -Condition $slipMatch.Success -Message 'Receipt page did not include a payment slip link.'
    $paymentId = [int]$slipMatch.Groups[1].Value
    $results['slip_payment_id'] = $paymentId

    $slipResponse = Invoke-FlowRequest -Session $session -Uri "$BaseUrl/process/view_payment_slip.php?payment_id=$paymentId"
    $results['slip_status'] = [int]$slipResponse.StatusCode
    $results['slip_content_type'] = [string]$slipResponse.Headers['Content-Type']
    Assert-True -Condition ($slipResponse.StatusCode -eq 200) -Message 'Payment slip endpoint did not return 200.'
    Assert-True -Condition ([string]$slipResponse.Headers['Content-Type'] -match 'image/png') -Message 'Payment slip endpoint did not return PNG content.'

    if (-not $KeepTestData) {
        $results['cleanup_flash'] = Cancel-Order -Session $session -BaseUrl $BaseUrl -OrderId $orderId -ReturnTo 'quick_service.php' -Reason 'smoke quick service transfer slip cleanup'
        Assert-NotBlank -Value $results['cleanup_flash'] -Label 'Cleanup'
    }

    $results.GetEnumerator() | ForEach-Object {
        "{0}: {1}" -f $_.Key, $_.Value
    }
} catch {
    if (-not $KeepTestData) {
        try {
            if ($orderId -gt 0) {
                $null = Cancel-Order -Session $session -BaseUrl $BaseUrl -OrderId $orderId -ReturnTo 'quick_service.php' -Reason 'smoke quick service transfer slip cleanup'
            }
        } catch {
        }
    }

    throw
} finally {
    if ($slipPath -ne '' -and (Test-Path -LiteralPath $slipPath)) {
        Remove-Item -LiteralPath $slipPath -Force
    }
}
