Option Strict On
Option Explicit On

Imports System
Imports System.Reflection
Imports Nexamas.UI.FloatRuntime
Imports Nexamas.UI.Motion

Namespace Nexamas.UI.Quality

    ''' <summary>
    ''' Behavioral proof that FloatRuntime owns and shuts down active transition runners at host dispose.
    ''' </summary>
    Friend NotInheritable Class MASFloatTransitionDisposeGate

        Private Sub New()
        End Sub

        Friend Shared Function Passes() As Boolean
            Dim previousMode As MASMotionReducedMotionMode = MASMotionSystem.ReducedMotionMode

            Try
                MASMotionSystem.ReducedMotionMode = MASMotionReducedMotionMode.FullMotion

                Return ProbeTransitionEngineShutdownCancelsRunner() AndAlso
                       ProbeMotionClockStopsSnapshotCallbacksAfterDispose() AndAlso
                       ProbeMotionClockSkipsUnregisteredSnapshotConsumer() AndAlso
                       ProbeRuntimeShutdownContractShape()
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(ex, "MASFloatTransitionDisposeGate")
                Return False
            Finally
                MASMotionSystem.ReducedMotionMode = previousMode
            End Try
        End Function

        Private Shared Function ProbeTransitionEngineShutdownCancelsRunner() As Boolean
            Dim clock As New MASMotionFrameClock("float-transition-dispose")
            Dim requestFrameCount As Integer = 0
            Dim completedCount As Integer = 0
            Dim engine As New MASFloatTransitionEngine(
                requestFrame:=Sub()
                                  requestFrameCount += 1
                              End Sub,
                motionFrameClock:=clock)
            Dim record As MASFloatLifecycleRecord = CreateProbeRecord()

            Try
                engine.RunOpenTransition(
                    record,
                    Sub()
                        completedCount += 1
                    End Sub)

                If engine.ActiveTransitionCount <> 1 Then Return False
                If clock.ActiveConsumerCount() <> 1 Then Return False
                If completedCount <> 0 Then Return False
                If requestFrameCount < 1 Then Return False

                engine.Shutdown()

                If engine.ActiveTransitionCount <> 0 Then Return False
                If clock.ActiveConsumerCount() <> 0 Then Return False
                If completedCount <> 0 Then Return False
                If record.HasActiveTransitionVisual Then Return False

                clock.AdvanceScheduledAnimations()

                If engine.ActiveTransitionCount <> 0 Then Return False
                If clock.ActiveConsumerCount() <> 0 Then Return False
                If completedCount <> 0 Then Return False

                Return True
            Finally
                engine.Dispose()
                clock.Dispose()
            End Try
        End Function

        Private Shared Function ProbeMotionClockStopsSnapshotCallbacksAfterDispose() As Boolean
            Dim clock As New MASMotionFrameClock("float-transition-dispose-snapshot")
            Dim ownerA As New Object()
            Dim ownerB As New Object()
            Dim requestFrameCount As Integer = 0
            Dim firstAdvanceCount As Integer = 0
            Dim secondAdvanceCount As Integer = 0

            Try
                clock.Register(
                    owner:=ownerA,
                    consumerName:="dispose-during-advance-a",
                    requestFrame:=Sub()
                                      requestFrameCount += 1
                                  End Sub,
                    advanceFrame:=Function(deltaMs As Integer)
                                      firstAdvanceCount += 1
                                      clock.Dispose()
                                      Return True
                                  End Function)

                clock.Register(
                    owner:=ownerB,
                    consumerName:="dispose-during-advance-b",
                    requestFrame:=Sub()
                                      requestFrameCount += 1
                                  End Sub,
                    advanceFrame:=Function(deltaMs As Integer)
                                      secondAdvanceCount += 1
                                      Return True
                                  End Function)

                If clock.ActiveConsumerCount() <> 2 Then Return False
                If requestFrameCount <> 2 Then Return False

                If clock.AdvanceScheduledAnimations() Then Return False

                If firstAdvanceCount <> 1 Then Return False
                If secondAdvanceCount <> 0 Then Return False
                If clock.ActiveConsumerCount() <> 0 Then Return False
                If requestFrameCount <> 2 Then Return False

                clock.AdvanceScheduledAnimations()

                If firstAdvanceCount <> 1 Then Return False
                If secondAdvanceCount <> 0 Then Return False
                If clock.ActiveConsumerCount() <> 0 Then Return False

                Return True
            Finally
                clock.Dispose()
            End Try
        End Function

        Private Shared Function ProbeMotionClockSkipsUnregisteredSnapshotConsumer() As Boolean
            Dim clock As New MASMotionFrameClock("float-transition-unregister-snapshot")
            Dim ownerA As New Object()
            Dim ownerB As New Object()
            Dim requestFrameCount As Integer = 0
            Dim firstAdvanceCount As Integer = 0
            Dim secondAdvanceCount As Integer = 0

            Try
                clock.Register(
                    owner:=ownerA,
                    consumerName:="unregister-during-advance-a",
                    requestFrame:=Sub()
                                      requestFrameCount += 1
                                  End Sub,
                    advanceFrame:=Function(deltaMs As Integer)
                                      firstAdvanceCount += 1
                                      clock.Unregister(ownerB)
                                      Return False
                                  End Function)

                clock.Register(
                    owner:=ownerB,
                    consumerName:="unregister-during-advance-b",
                    requestFrame:=Sub()
                                      requestFrameCount += 1
                                  End Sub,
                    advanceFrame:=Function(deltaMs As Integer)
                                      secondAdvanceCount += 1
                                      Return True
                                  End Function)

                If clock.ActiveConsumerCount() <> 2 Then Return False
                If requestFrameCount <> 2 Then Return False

                If clock.AdvanceScheduledAnimations() Then Return False

                If firstAdvanceCount <> 1 Then Return False
                If secondAdvanceCount <> 0 Then Return False
                If clock.ActiveConsumerCount() <> 0 Then Return False
                If requestFrameCount <> 2 Then Return False

                Return True
            Finally
                clock.Dispose()
            End Try
        End Function

        Private Shared Function ProbeRuntimeShutdownContractShape() As Boolean
            If Not GetType(IDisposable).IsAssignableFrom(GetType(MASFloatRuntime)) Then Return False
            If Not GetType(IDisposable).IsAssignableFrom(GetType(MASFloatTransitionEngine)) Then Return False

            If GetFriendInstanceMethod(GetType(MASFloatRuntime), "Shutdown") Is Nothing Then Return False
            If GetFriendInstanceMethod(GetType(MASFloatCoordinator), "Shutdown") Is Nothing Then Return False
            If GetFriendInstanceMethod(GetType(MASFloatTransitionEngine), "Shutdown") Is Nothing Then Return False
            If GetFriendInstanceProperty(GetType(MASFloatTransitionEngine), "ActiveTransitionCount") Is Nothing Then Return False

            If Not MethodCalls(GetType(MASFloatRuntime), "Shutdown", GetType(MASFloatCoordinator), "Shutdown") Then Return False
            If Not MethodCalls(GetType(MASFloatCoordinator), "Shutdown", GetType(MASFloatTransitionEngine), "Shutdown") Then Return False
            If Not MethodCalls(GetType(Nexamas.UI.Host.MASSkiaHostFloatRuntimeCoordinator), "Dispose", GetType(MASFloatRuntime), "Dispose") Then Return False

            Return True
        End Function

        Private Shared Function CreateProbeRecord() As MASFloatLifecycleRecord
            Dim presentation As New MASFloatPresentation() With {
                .Placement = MASFloatPlacement.Center,
                .Transition = MASFloatTransitionKind.Fade,
                .CloseTransition = MASFloatTransitionKind.Fade,
                .DurationMs = 10000,
                .Easing = MASFloatEasing.Linear
            }

            Dim descriptor As New MASFloatDescriptor() With {
                .Key = "float-transition-dispose-probe",
                .DisplayName = "Float transition dispose probe",
                .Category = MASFloatCategory.Popup,
                .DefaultSlot = MASFloatSlot.Popup,
                .DefaultScope = MASFloatScope.Owner,
                .DefaultLayer = MASFloatLayerLevel.Interactive,
                .DefaultPresentation = presentation.Clone(),
                .RequiresOwnerKey = False
            }

            Dim request As New MASFloatRequest() With {
                .FloatKey = descriptor.Key,
                .OwnerKey = "float-transition-dispose-owner",
                .RequestedSlot = MASFloatSlot.Popup,
                .RequestedScope = MASFloatScope.Owner,
                .RequestedLayer = MASFloatLayerLevel.Interactive,
                .RequestedPresentation = presentation.Clone()
            }

            Dim resolved As MASFloatResolvedRequest = MASFloatResolvedRequest.Create(descriptor, request)
            Dim ownerToken As MASFloatOwnerToken = MASFloatOwnerToken.FromKey(request.OwnerKey)
            Dim handle As New MASFloatHandle(
                id:=1L,
                generation:=1L,
                floatKey:=descriptor.Key,
                ownerKey:=request.OwnerKey,
                ownerToken:=ownerToken,
                slot:=MASFloatSlot.Popup,
                layer:=MASFloatLayerLevel.Interactive,
                scope:=MASFloatScope.Owner,
                initialState:=MASFloatLifecycleState.Opening,
                requestClose:=Function(h As MASFloatHandle, r As MASFloatCloseReason) False)

            Return New MASFloatLifecycleRecord(handle, resolved)
        End Function

        Private Shared Function GetFriendInstanceMethod(targetType As Type,
                                                        methodName As String) As MethodInfo
            If targetType Is Nothing OrElse String.IsNullOrWhiteSpace(methodName) Then Return Nothing

            Return targetType.GetMethod(
                methodName,
                BindingFlags.Instance Or BindingFlags.Public Or BindingFlags.NonPublic)
        End Function

        Private Shared Function GetFriendInstanceProperty(targetType As Type,
                                                          propertyName As String) As PropertyInfo
            If targetType Is Nothing OrElse String.IsNullOrWhiteSpace(propertyName) Then Return Nothing

            Return targetType.GetProperty(
                propertyName,
                BindingFlags.Instance Or BindingFlags.Public Or BindingFlags.NonPublic)
        End Function

        Private Shared Function MethodCalls(callerType As Type,
                                            callerName As String,
                                            calleeType As Type,
                                            calleeName As String) As Boolean
            Dim caller As MethodInfo = GetFriendInstanceMethod(callerType, callerName)
            Dim callee As MethodInfo = GetFriendInstanceMethod(calleeType, calleeName)

            If caller Is Nothing OrElse callee Is Nothing Then Return False

            Dim body As MethodBody = caller.GetMethodBody()
            If body Is Nothing Then Return False

            Dim il As Byte() = body.GetILAsByteArray()
            If il Is Nothing OrElse il.Length = 0 Then Return False

            Dim calleeToken As Integer = callee.MetadataToken

            For i As Integer = 0 To il.Length - 5
                Dim opCode As Byte = il(i)
                If opCode = &H28 OrElse opCode = &H6F Then
                    Dim token As Integer = BitConverter.ToInt32(il, i + 1)
                    If token = calleeToken Then Return True
                End If
            Next

            Return False
        End Function

    End Class

End Namespace
