$ErrorActionPreference = "Stop"

$base = "http://127.0.0.1:8755/api/v1"
$origin = "http://127.0.0.1:9346"

function Invoke-Json {
    param(
        [string]$Method,
        [string]$Path,
        [string]$Token,
        [object]$Body = $null,
        [hashtable]$ExtraHeaders = @{}
    )
    $headers = @{}
    if ($Token) {
        $headers.Authorization = "Bearer $Token"
    }
    foreach ($key in $ExtraHeaders.Keys) {
        $headers[$key] = $ExtraHeaders[$key]
    }
    $parameters = @{
        Method = $Method
        Uri = "$base$Path"
        Headers = $headers
        UseBasicParsing = $true
    }
    if ($null -ne $Body) {
        $parameters.ContentType = "application/json"
        $parameters.Body = ($Body | ConvertTo-Json -Depth 10 -Compress)
    }
    try {
        $response = Invoke-WebRequest @parameters
        return @{
            Status = [int]$response.StatusCode
            Body = ($response.Content | ConvertFrom-Json)
            Headers = $response.Headers
        }
    }
    catch {
        $response = $_.Exception.Response
        $content = $_.ErrorDetails.Message
        if (-not $content) {
            $reader = New-Object System.IO.StreamReader($response.GetResponseStream())
            $content = $reader.ReadToEnd()
            $reader.Dispose()
        }
        return @{
            Status = [int]$response.StatusCode
            Body = ($content | ConvertFrom-Json)
            Headers = $response.Headers
        }
    }
}

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

function Upload-Document {
    param(
        [string]$Token,
        [string]$File,
        [hashtable]$Metadata,
        [bool]$Attachment = $false
    )
    $path = if ($Attachment) { "/attachments" } else { "/documents" }
    $metadataJson = $Metadata | ConvertTo-Json -Depth 10 -Compress
    $metadataFile = [System.IO.Path]::GetTempFileName()
    try {
        [System.IO.File]::WriteAllText(
            $metadataFile,
            $metadataJson,
            [System.Text.UTF8Encoding]::new($false)
        )
        $output = & curl.exe -sS -X POST "$base$path" `
            -H "Authorization: Bearer $Token" `
            -F "file=@$File" `
            -F "metadata=<$metadataFile"
        if ($LASTEXITCODE -ne 0) {
            throw "curl上传失败"
        }
    }
    finally {
        Remove-Item -LiteralPath $metadataFile -Force -ErrorAction SilentlyContinue
    }
    $body = $output | ConvertFrom-Json
    Assert-True ($body.code -eq "OK") "上传失败：$output"
    return $body.data
}

function Issue-Token {
    param([string]$Username)
    $response = Invoke-Json POST "/auth/login" "" @{
        username = $Username
        password = $env:DMS_TEST_USER_PASSWORD
        keepSignedIn = $false
    }
    Assert-True ($response.Status -eq 200) "$Username 登录失败"
    return $response.Body.data.accessToken
}

$adminToken = Issue-Token "admin"
$userToken = Issue-Token "user"
$auditorToken = Issue-Token "auditor"

$health = Invoke-Json GET "/health" "" $null @{Origin = $origin}
Assert-True ($health.Status -eq 200) "健康检查失败"
Assert-True (
    $health.Headers["Access-Control-Expose-Headers"] -match "Content-Disposition" -and
    $health.Headers["Access-Control-Expose-Headers"] -match "X-Request-Id"
) "CORS暴露响应头缺失"

$tree = Invoke-Json GET "/categories/tree" $adminToken
Assert-True ($tree.Status -eq 200) "分类树查询失败"
$categoryId = [string]$tree.Body.data[0].id
$sampleFile = (
    Get-ChildItem -LiteralPath (
        Join-Path $PSScriptRoot "..\..\dms-test-storage\original"
    ) -Filter "*.docx" -File | Select-Object -First 1
).FullName
Assert-True ([bool]$sampleFile) "未找到联调上传样本文件"

$main = Upload-Document $adminToken $sampleFile @{
    documentName = "F8_RECYCLE_MAIN"
    documentType = "MAIN"
    summary = "B8 HTTP MAIN"
    categoryId = $categoryId
    securityLevel = "INTERNAL"
    visibilityType = "CUSTOM"
    status = "PUBLISHED"
    tags = @("B8", "F8")
}
$attachment = Upload-Document $adminToken $sampleFile @{
    documentName = "F8_RECYCLE_ATTACHMENT"
    attachmentType = "OTHER"
    summary = "B8 HTTP ATTACHMENT"
    tags = @("B8", "F8")
} $true

$users = Invoke-Json GET "/users?keyword=user&page=1&pageSize=20" $adminToken
$subjectUser = $users.Body.data.items | Where-Object {$_.username -eq "user"} |
    Select-Object -First 1
$permission = Invoke-Json PUT "/documents/$($main.id)/permissions" $adminToken @{
    visibilityType = "CUSTOM"
    documentRowVersion = $main.rowVersion
    entries = @(
        @{
            subjectType = "USER"
            subjectId = [string]$subjectUser.id
            canView = $true
            canDownload = $true
            canEdit = $false
            canManagePermission = $false
            canDelete = $false
        }
    )
}
Assert-True ($permission.Status -eq 200) "权限配置失败"
$binding = Invoke-Json POST "/main-plans/$($main.id)/attachments/bind" $adminToken @{
    attachmentIds = @([string]$attachment.id)
    mainPlanRowVersion = $permission.Body.data.documentRowVersion
}
Assert-True ($binding.Status -eq 200) "附件挂载失败"
$main.rowVersion = $binding.Body.data.mainPlanRowVersion

$deleteMain = Invoke-Json DELETE "/documents/$($main.id)?rowVersion=$($main.rowVersion)" $adminToken
Assert-True ($deleteMain.Status -eq 200) "主案删除失败"
$bin = Invoke-Json GET "/recycle-bin/documents?keyword=F8_RECYCLE_MAIN" $adminToken $null @{Origin = $origin}
Assert-True ($bin.Status -eq 200 -and $bin.Body.data.total -eq 1) "主案未进入回收站"
$deletedMain = $bin.Body.data.items[0]
Assert-True ($null -ne $deletedMain.deletedBy) "删除人快照缺失"

$wrongVersion = Invoke-Json POST "/recycle-bin/documents/$($main.id)/restore" $adminToken @{
    rowVersion = [Math]::Max(0, [int]$deletedMain.rowVersion - 1)
}
Assert-True (
    $wrongVersion.Status -eq 409 -and
    $wrongVersion.Body.code -eq "DATA_VERSION_CONFLICT"
) "rowVersion冲突未按契约返回"

$restoreMain = Invoke-Json POST "/recycle-bin/documents/$($main.id)/restore" $adminToken @{
    rowVersion = [int]$deletedMain.rowVersion
}
Assert-True ($restoreMain.Status -eq 200) "主案恢复失败"
Assert-True (
    $restoreMain.Body.data.restoredPermissionCount -eq 1 -and
    $restoreMain.Body.data.restoredBindingCount -eq 1
) "主案ACL或挂载未恢复"

$restoredPermissions = Invoke-Json GET "/documents/$($main.id)/permissions" $adminToken
$restoredAttachments = Invoke-Json GET "/main-plans/$($main.id)/attachments" $adminToken
Assert-True ($restoredPermissions.Body.data.entries.Count -eq 1) "恢复后ACL不同步"
Assert-True ($restoredAttachments.Body.data.total -eq 1) "恢复后挂载不同步"

$sub = Upload-Document $adminToken $sampleFile @{
    documentName = "F8_RECYCLE_SUB_PLAN"
    documentType = "SUB_PLAN"
    summary = "B8 HTTP SUB PLAN"
    categoryId = $categoryId
    securityLevel = "INTERNAL"
    visibilityType = "CUSTOM"
    status = "PUBLISHED"
    tags = @("B8", "F8")
    parentDocumentId = [string]$main.id
}
$deleteSub = Invoke-Json DELETE "/documents/$($sub.id)?rowVersion=$($sub.rowVersion)" $adminToken
Assert-True ($deleteSub.Status -eq 200) "子方案删除失败"
$subBin = Invoke-Json GET "/recycle-bin/documents?keyword=F8_RECYCLE_SUB_PLAN" $adminToken
$deletedSub = $subBin.Body.data.items[0]

$currentMainPermission = Invoke-Json GET "/documents/$($main.id)/permissions" $adminToken
$deleteParent = Invoke-Json DELETE (
    "/documents/$($main.id)?rowVersion=" +
    $currentMainPermission.Body.data.documentRowVersion
) $adminToken
Assert-True ($deleteParent.Status -eq 200) "父主案二次删除失败"
$invalidParent = Invoke-Json POST "/recycle-bin/documents/$($sub.id)/restore" $adminToken @{
    rowVersion = [int]$deletedSub.rowVersion
}
Assert-True (
    $invalidParent.Status -eq 409 -and
    $invalidParent.Body.code -eq "RESTORE_PARENT_INVALID"
) "无效父主案冲突未返回"

$mainBinAgain = Invoke-Json GET "/recycle-bin/documents?keyword=F8_RECYCLE_MAIN" $adminToken
$restoreMainAgain = Invoke-Json POST "/recycle-bin/documents/$($main.id)/restore" $adminToken @{
    rowVersion = [int]$mainBinAgain.Body.data.items[0].rowVersion
}
Assert-True ($restoreMainAgain.Status -eq 200) "主案二次恢复失败"
$restoreSub = Invoke-Json POST "/recycle-bin/documents/$($sub.id)/restore" $adminToken @{
    rowVersion = [int]$deletedSub.rowVersion
}
Assert-True ($restoreSub.Status -eq 200) "子方案恢复失败"

$mainPermissionAfterSub = Invoke-Json GET "/documents/$($main.id)/permissions" $adminToken
$unbind = Invoke-Json DELETE (
    "/main-plans/$($main.id)/attachments/$($attachment.id)" +
    "?mainPlanRowVersion=" +
    $mainPermissionAfterSub.Body.data.documentRowVersion
) $adminToken
Assert-True ($unbind.Status -eq 200) "解除挂载失败"
$deleteAttachment = Invoke-Json DELETE (
    "/attachments/$($attachment.id)?rowVersion=$($attachment.rowVersion)"
) $adminToken
Assert-True ($deleteAttachment.Status -eq 200) "附件删除失败"
$attachmentBin = Invoke-Json GET "/recycle-bin/documents?keyword=F8_RECYCLE_ATTACHMENT" $adminToken
$restoreAttachment = Invoke-Json POST (
    "/recycle-bin/documents/$($attachment.id)/restore"
) $adminToken @{
    rowVersion = [int]$attachmentBin.Body.data.items[0].rowVersion
}
Assert-True ($restoreAttachment.Status -eq 200) "附件恢复失败"
Assert-True (
    $restoreAttachment.Body.data.restoredBindingCount -eq 0 -and
    $restoreAttachment.Body.data.document.mountedPlanCount -eq 0
) "附件错误恢复了历史挂载"

foreach ($token in @($userToken, $auditorToken)) {
    $forbidden = Invoke-Json GET "/recycle-bin/documents" $token
    Assert-True (
        $forbidden.Status -eq 403 -and $forbidden.Body.code -eq "FORBIDDEN"
    ) "非管理员未被拒绝"
}
$oldPath = Invoke-Json POST "/documents/$($main.id)/restore" $adminToken @{
    rowVersion = 0
}
Assert-True ($oldPath.Status -eq 404) "旧恢复路径未保持404"

$restoreAudits = Invoke-Json GET (
    "/audit/logs?actionType=RESTORE_DOCUMENT&page=1&pageSize=100"
) $auditorToken
Assert-True ($restoreAudits.Body.data.total -ge 3) "恢复审计缺失"
$viewAudits = Invoke-Json GET (
    "/audit/logs?actionType=VIEW_DOCUMENT&targetId=$($main.id)&page=1&pageSize=100"
) $auditorToken
Assert-True ($viewAudits.Body.data.total -eq 0) "恢复响应产生了VIEW_DOCUMENT"

# 最终仅保留三条F8回收站样本，全部通过正式DELETE接口产生。
$deleteSubFinal = Invoke-Json DELETE (
    "/documents/$($sub.id)?rowVersion=$($restoreSub.Body.data.document.rowVersion)"
) $adminToken
Assert-True ($deleteSubFinal.Status -eq 200) "F8子方案最终删除失败"
$mainPermissionFinal = Invoke-Json GET "/documents/$($main.id)/permissions" $adminToken
$deleteMainFinal = Invoke-Json DELETE (
    "/documents/$($main.id)?rowVersion=" +
    $mainPermissionFinal.Body.data.documentRowVersion
) $adminToken
Assert-True ($deleteMainFinal.Status -eq 200) "F8主案最终删除失败"
$deleteAttachmentFinal = Invoke-Json DELETE (
    "/attachments/$($attachment.id)" +
    "?rowVersion=$($restoreAttachment.Body.data.document.rowVersion)"
) $adminToken
Assert-True ($deleteAttachmentFinal.Status -eq 200) "F8附件最终删除失败"

$finalBin = Invoke-Json GET "/recycle-bin/documents?page=1&pageSize=100" $adminToken
$finalItems = @($finalBin.Body.data.items | Where-Object {
    $_.documentName -in @(
        "F8_RECYCLE_MAIN",
        "F8_RECYCLE_SUB_PLAN",
        "F8_RECYCLE_ATTACHMENT"
    )
})
Assert-True ($finalItems.Count -eq 3) "F8三条回收站样本不完整"

[pscustomobject]@{
    mainId = [string]$main.id
    subPlanId = [string]$sub.id
    attachmentId = [string]$attachment.id
    recycleBinTotal = $finalBin.Body.data.total
    restoreAuditTotal = $restoreAudits.Body.data.total
    corsExposeHeaders = $health.Headers["Access-Control-Expose-Headers"]
} | ConvertTo-Json -Compress
