Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Globalization
Imports System.IO
Imports System.Linq

Namespace Nexamas.UI.Verification

    ''' <summary>
    ''' Readiness gate for the Nexamas UI render verification system.
    ''' It validates target identity, scenario uniqueness, deterministic file names, capture-scene ownership,
    ''' dimensions, DPI, theme keys, and baseline-store ownership before capture starts.
    ''' </summary>
    Friend NotInheritable Class MASRenderVerificationGate

        Friend Const EmptyTargetSetCode As String = "MASRenderVerification.EmptyTargetSet"
        Friend Const MissingStoreCode As String = "MASRenderVerification.MissingBaselineStore"
        Friend Const EmptyTargetIdCode As String = "MASRenderVerification.EmptyTargetId"
        Friend Const EmptyTargetDisplayNameCode As String = "MASRenderVerification.EmptyTargetDisplayName"
        Friend Const DuplicateTargetCode As String = "MASRenderVerification.DuplicateTarget"
        Friend Const EmptyScenarioSetCode As String = "MASRenderVerification.EmptyScenarioSet"
        Friend Const InvalidScenarioCode As String = "MASRenderVerification.InvalidScenario"
        Friend Const EmptyScenarioDisplayNameCode As String = "MASRenderVerification.EmptyScenarioDisplayName"
        Friend Const DuplicateScenarioCode As String = "MASRenderVerification.DuplicateScenario"
        Friend Const DuplicateOutputFileCode As String = "MASRenderVerification.DuplicateOutputFileName"
        Friend Const MissingCaptureSceneCode As String = "MASRenderVerification.MissingCaptureScene"
        Friend Const UnknownThemeCode As String = "MASRenderVerification.UnknownThemeKey"
        Friend Const UnsupportedDpiCode As String = "MASRenderVerification.UnsupportedDpiScale"
        Friend Const UnsafeDimensionsCode As String = "MASRenderVerification.UnsafeDimensions"

        Private Const MinimumDimensionPx As Integer = 16
        Private Const MaximumDimensionPx As Integer = 4096
        Private Const MaximumPixelBudget As Integer = 4194304

        Private Shared ReadOnly AllowedDpiScales As Single() = New Single() {1.0F, 1.25F, 1.5F, 2.0F}

        Private Sub New()
        End Sub

        Friend Shared Function Evaluate(targets As IEnumerable(Of MASRenderVerificationTarget), store As MASRenderBaselineStore) As MASRenderVerificationReport
            Dim targetList = If(targets, Array.Empty(Of MASRenderVerificationTarget)()).Where(Function(target) target IsNot Nothing).ToArray()
            Dim findings = New List(Of MASRenderVerificationFinding)()
            Dim targetIds = New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
            Dim scenarioIds = New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
            Dim outputFileNames = New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
            Dim scenarioCount As Integer = 0

            If targetList.Length = 0 Then
                findings.Add(New MASRenderVerificationFinding(EmptyTargetSetCode, "Render verification requires at least one target.", MASRenderVerificationFindingSeverity.Error))
            End If

            If store Is Nothing Then
                findings.Add(New MASRenderVerificationFinding(MissingStoreCode, "Render verification requires an owned baseline store.", MASRenderVerificationFindingSeverity.Error))
            End If

            For Each target In targetList
                If String.IsNullOrWhiteSpace(target.Id) Then
                    findings.Add(New MASRenderVerificationFinding(EmptyTargetIdCode, "A render verification target has no stable id.", MASRenderVerificationFindingSeverity.Error))
                ElseIf Not targetIds.Add(target.Id) Then
                    findings.Add(New MASRenderVerificationFinding(DuplicateTargetCode, "Target id appears more than once: " & target.Id, MASRenderVerificationFindingSeverity.Error))
                End If

                If String.IsNullOrWhiteSpace(target.DisplayName) Then
                    findings.Add(New MASRenderVerificationFinding(EmptyTargetDisplayNameCode, "Target '" & target.Id & "' has no display name.", MASRenderVerificationFindingSeverity.Error))
                End If

                If target.ScenarioCount = 0 Then
                    findings.Add(New MASRenderVerificationFinding(EmptyScenarioSetCode, "Target '" & target.Id & "' has no scenarios.", MASRenderVerificationFindingSeverity.Error))
                End If

                For Each scenario In target.Scenarios
                    scenarioCount += 1
                    ValidateScenario(target, scenario, scenarioIds, outputFileNames, findings)
                Next
            Next

            Return New MASRenderVerificationReport(targetList.Length, scenarioCount, findings)
        End Function

        Private Shared Sub ValidateScenario(target As MASRenderVerificationTarget,
                                            scenario As MASRenderVerificationScenario,
                                            scenarioIds As HashSet(Of String),
                                            outputFileNames As HashSet(Of String),
                                            findings As List(Of MASRenderVerificationFinding))
            If scenario Is Nothing Then
                findings.Add(New MASRenderVerificationFinding(InvalidScenarioCode, "Target '" & target.Id & "' contains a null scenario.", MASRenderVerificationFindingSeverity.Error))
                Return
            End If

            If Not scenario.IsValid Then
                findings.Add(New MASRenderVerificationFinding(InvalidScenarioCode, "Scenario '" & scenario.Id & "' is missing required identity, component, dimensions, theme, state, or DPI facts.", MASRenderVerificationFindingSeverity.Error))
            End If

            If String.IsNullOrWhiteSpace(scenario.DisplayName) Then
                findings.Add(New MASRenderVerificationFinding(EmptyScenarioDisplayNameCode, "Scenario '" & scenario.Id & "' has no display name.", MASRenderVerificationFindingSeverity.Error))
            End If

            If Not String.IsNullOrWhiteSpace(scenario.Id) Then
                If Not scenarioIds.Add(scenario.Id) Then
                    findings.Add(New MASRenderVerificationFinding(DuplicateScenarioCode, "Scenario id appears more than once: " & scenario.Id, MASRenderVerificationFindingSeverity.Error))
                End If

                Dim outputFileName As String = BuildOutputFileName(scenario.Id)
                If Not outputFileNames.Add(outputFileName) Then
                    findings.Add(New MASRenderVerificationFinding(DuplicateOutputFileCode, "Scenario output file name collides after sanitizing: " & outputFileName, MASRenderVerificationFindingSeverity.Error))
                End If
            End If

            If Not String.IsNullOrWhiteSpace(scenario.ComponentKey) AndAlso Not MASRenderVerificationShowcaseCapture.HasRegisteredScene(scenario.ComponentKey) Then
                findings.Add(New MASRenderVerificationFinding(MissingCaptureSceneCode, "Scenario '" & scenario.Id & "' uses component key '" & scenario.ComponentKey & "' without a registered render-verification capture scene.", MASRenderVerificationFindingSeverity.Error))
            End If

            If Not String.IsNullOrWhiteSpace(scenario.ThemeKey) AndAlso Not MASRenderVerificationThemeResolver.IsKnownThemeKey(scenario.ThemeKey) Then
                findings.Add(New MASRenderVerificationFinding(UnknownThemeCode, "Scenario '" & scenario.Id & "' uses unknown render-verification theme key '" & scenario.ThemeKey & "'.", MASRenderVerificationFindingSeverity.Error))
            End If

            If scenario.DpiScale > 0.0F AndAlso Not IsAllowedDpiScale(scenario.DpiScale) Then
                findings.Add(New MASRenderVerificationFinding(UnsupportedDpiCode, "Scenario '" & scenario.Id & "' uses unsupported DPI scale " & scenario.DpiScale.ToString(CultureInfo.InvariantCulture) & ".", MASRenderVerificationFindingSeverity.Error))
            End If

            If Not HasSafeDimensions(scenario.WidthPx, scenario.HeightPx) Then
                findings.Add(New MASRenderVerificationFinding(UnsafeDimensionsCode, "Scenario '" & scenario.Id & "' has unsafe pixel dimensions " & scenario.WidthPx.ToString(CultureInfo.InvariantCulture) & "x" & scenario.HeightPx.ToString(CultureInfo.InvariantCulture) & ".", MASRenderVerificationFindingSeverity.Error))
            End If
        End Sub

        Private Shared Function IsAllowedDpiScale(value As Single) As Boolean
            For Each allowed In AllowedDpiScales
                If Math.Abs(value - allowed) < 0.001F Then Return True
            Next

            Return False
        End Function

        Private Shared Function HasSafeDimensions(widthPx As Integer, heightPx As Integer) As Boolean
            If widthPx < MinimumDimensionPx OrElse heightPx < MinimumDimensionPx Then Return False
            If widthPx > MaximumDimensionPx OrElse heightPx > MaximumDimensionPx Then Return False

            Dim pixels As Long = CLng(widthPx) * CLng(heightPx)
            Return pixels <= MaximumPixelBudget
        End Function

        Private Shared Function BuildOutputFileName(scenarioId As String) As String
            Dim clean = If(scenarioId, String.Empty).Trim()
            For Each ch In Path.GetInvalidFileNameChars()
                clean = clean.Replace(ch, "_"c)
            Next

            If clean.Length = 0 Then clean = "unnamed"
            Return clean & ".png"
        End Function

    End Class

End Namespace
