Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.IO
Imports System.Linq
Imports System.Xml.Linq

Namespace Nexamas.UI.Certification

    ''' <summary>
    ''' File-system gate for the commercial Nexamas UI quality/product tree.
    ''' It verifies that project-declared assets and the canonical quality runner assets exist before a release can claim certification evidence.
    ''' </summary>
    Friend NotInheritable Class MASQualityAssetPresenceGate

        Friend Const MissingProjectRootCode As String = "MASQualityAssetPresence.MissingProjectRoot"
        Friend Const MissingProjectFileCode As String = "MASQualityAssetPresence.MissingProjectFile"
        Friend Const MissingAssetCode As String = "MASQualityAssetPresence.MissingAsset"
        Friend Const AssetPresentCode As String = "MASQualityAssetPresence.AssetPresent"

        Private Sub New()
        End Sub

        Friend Shared Function Evaluate(projectRoot As String) As MASQualityAssetPresenceReport
            Dim root As String = NormalizeRoot(projectRoot)
            Dim entries As New List(Of MASQualityAssetPresenceEntry)()

            If String.IsNullOrWhiteSpace(root) OrElse Not Directory.Exists(root) Then
                entries.Add(MASQualityAssetPresenceEntry.Missing(".", "ProjectRoot", MissingProjectRootCode))
                Return New MASQualityAssetPresenceReport(entries)
            End If

            Dim requiredAssets As IReadOnlyList(Of String) = CreateRequiredAssetPaths(root)
            For Each relativePath As String In requiredAssets
                Dim fullPath As String = Path.Combine(root, relativePath.Replace("/"c, Path.DirectorySeparatorChar))
                If File.Exists(fullPath) Then
                    entries.Add(MASQualityAssetPresenceEntry.Present(relativePath, "RequiredAsset", AssetPresentCode))
                Else
                    entries.Add(MASQualityAssetPresenceEntry.Missing(relativePath, "RequiredAsset", MissingAssetCode))
                End If
            Next

            Return New MASQualityAssetPresenceReport(entries)
        End Function

        Friend Shared Function CreateRequiredAssetPaths(projectRoot As String) As IReadOnlyList(Of String)
            Dim paths As New List(Of String)()
            AddCanonicalAssets(paths)
            AddProjectDeclaredAssets(paths, projectRoot)

            paths.Sort(StringComparer.OrdinalIgnoreCase)
            Return New ReadOnlyCollection(Of String)(paths)
        End Function

        Private Shared Sub AddCanonicalAssets(paths As List(Of String))
            AddUnique(paths, ".github/workflows/nexamas-ui-ci.yml")
            AddUnique(paths, "Nexamas.UI.vbproj")
            AddUnique(paths, "Nexamas.UI.nuspec")
            AddUnique(paths, "Nexamas.UI.sln")
            AddUnique(paths, "README.md")
            AddUnique(paths, "LICENSE.md")
            AddUnique(paths, "THIRD_PARTY_NOTICES.md")
            AddUnique(paths, "NuGet.Config")
            AddUnique(paths, "packages.config")
            AddUnique(paths, "package.version")
            AddUnique(paths, "app.config")
            AddUnique(paths, "buildTransitive/Nexamas.UI.targets")
            AddUnique(paths, "docs/QUALITY.md")
            AddUnique(paths, "docs/output/OUTPUT_PRODUCT_API.md")
            AddUnique(paths, "tools/quality/Run-NexamasUIQuality.ps1")
            AddUnique(paths, "eng/ci/Invoke-NexamasUICommercialGate.ps1")
            AddUnique(paths, "eng/ci/Invoke-NexamasUIReleaseLockdown.ps1")
            AddUnique(paths, "eng/ci/Pack-Nexamas.UI.ps1")
            AddUnique(paths, "eng/ci/Test-NexamasUIPackedConsumer.ps1")
            AddUnique(paths, "eng/ci/Write-NexamasUICiProof.ps1")
            AddUnique(paths, "eng/tests/Nexamas.UI.QualityManifest.json")
            AddUnique(paths, "eng/tests/Run-NexamasUISdkHarness.ps1")
            AddUnique(paths, "eng/tests/Run-NexamasUISampleProof.ps1")
            AddUnique(paths, "eng/tests/Run-NexamasUITestSuite.ps1")
            AddUnique(paths, "samples/MinimalConsumerApp/MainForm.vb")
            AddUnique(paths, "samples/MinimalConsumerApp/MinimalConsumerApp.vbproj")
            AddUnique(paths, "samples/MinimalConsumerApp/README.md")
        End Sub

        Private Shared Sub AddProjectDeclaredAssets(paths As List(Of String), projectRoot As String)
            If String.IsNullOrWhiteSpace(projectRoot) Then Return

            Dim projectFile As String = Path.Combine(projectRoot, "Nexamas.UI.vbproj")
            If Not File.Exists(projectFile) Then
                AddUnique(paths, "Nexamas.UI.vbproj")
                Return
            End If

            Dim document As XDocument = XDocument.Load(projectFile)
            Dim includeElements = document.Descendants().Where(Function(element) IsProjectAssetElement(element.Name.LocalName))

            For Each element As XElement In includeElements
                Dim includeAttribute As XAttribute = element.Attribute("Include")
                If includeAttribute Is Nothing Then Continue For

                Dim relativePath As String = NormalizeRelativePath(includeAttribute.Value)
                If relativePath.Length = 0 Then Continue For
                If relativePath.Contains("$(") OrElse relativePath.Contains("*") Then Continue For

                AddUnique(paths, relativePath)
            Next
        End Sub


        Private Shared Function IsProjectAssetElement(localName As String) As Boolean
            Return String.Equals(localName, "Compile", StringComparison.OrdinalIgnoreCase) OrElse String.Equals(localName, "None", StringComparison.OrdinalIgnoreCase) OrElse String.Equals(localName, "EmbeddedResource", StringComparison.OrdinalIgnoreCase)
        End Function
        Private Shared Function NormalizeRoot(value As String) As String
            If String.IsNullOrWhiteSpace(value) Then Return String.Empty
            Return Path.GetFullPath(value.Trim())
        End Function

        Private Shared Function NormalizeRelativePath(value As String) As String
            If String.IsNullOrWhiteSpace(value) Then Return String.Empty
            Return value.Trim().Replace("\"c, "/"c)
        End Function

        Private Shared Sub AddUnique(paths As List(Of String), value As String)
            Dim normalized As String = NormalizeRelativePath(value)
            If normalized.Length = 0 Then Return

            For Each existing As String In paths
                If String.Equals(existing, normalized, StringComparison.OrdinalIgnoreCase) Then Return
            Next

            paths.Add(normalized)
        End Sub

    End Class

    Friend NotInheritable Class MASQualityAssetPresenceReport
        Friend Sub New(entries As IEnumerable(Of MASQualityAssetPresenceEntry))
            Dim normalized As New List(Of MASQualityAssetPresenceEntry)()
            If entries IsNot Nothing Then
                For Each entry As MASQualityAssetPresenceEntry In entries
                    If entry IsNot Nothing Then normalized.Add(entry)
                Next
            End If

            Me.Entries = New ReadOnlyCollection(Of MASQualityAssetPresenceEntry)(normalized)
        End Sub

        Friend ReadOnly Property Entries As IReadOnlyList(Of MASQualityAssetPresenceEntry)

        Friend ReadOnly Property RequiredAssetCount As Integer
            Get
                Return Entries.Count
            End Get
        End Property

        Friend ReadOnly Property MissingAssetCount As Integer
            Get
                Dim count As Integer = 0
                For Each entry As MASQualityAssetPresenceEntry In Entries
                    If Not entry.Exists Then count += 1
                Next
                Return count
            End Get
        End Property

        Friend ReadOnly Property HasMissingAssets As Boolean
            Get
                Return MissingAssetCount > 0
            End Get
        End Property

        Friend ReadOnly Property Status As MASQualityAssetPresenceStatus
            Get
                If HasMissingAssets Then Return MASQualityAssetPresenceStatus.MissingAssets
                Return MASQualityAssetPresenceStatus.Verified
            End Get
        End Property

        Friend Function CreateMissingAssetList() As IReadOnlyList(Of String)
            Dim missing As New List(Of String)()
            For Each entry As MASQualityAssetPresenceEntry In Entries
                If Not entry.Exists Then missing.Add(entry.RelativePath)
            Next
            Return New ReadOnlyCollection(Of String)(missing)
        End Function
    End Class

    Friend NotInheritable Class MASQualityAssetPresenceEntry
        Private Sub New(relativePath As String, source As String, code As String, exists As Boolean)
            Me.RelativePath = NormalizeText(relativePath)
            Me.Source = NormalizeText(source)
            Me.Code = NormalizeText(code)
            Me.Exists = exists
        End Sub

        Friend ReadOnly Property RelativePath As String
        Friend ReadOnly Property Source As String
        Friend ReadOnly Property Code As String
        Friend ReadOnly Property Exists As Boolean

        Friend Shared Function Present(relativePath As String, source As String, code As String) As MASQualityAssetPresenceEntry
            Return New MASQualityAssetPresenceEntry(relativePath, source, code, exists:=True)
        End Function

        Friend Shared Function Missing(relativePath As String, source As String, code As String) As MASQualityAssetPresenceEntry
            Return New MASQualityAssetPresenceEntry(relativePath, source, code, exists:=False)
        End Function

        Private Shared Function NormalizeText(value As String) As String
            Return If(value, String.Empty).Trim()
        End Function
    End Class

    Friend Enum MASQualityAssetPresenceStatus
        Verified = 0
        MissingAssets = 1
    End Enum

End Namespace
