// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Drawing;
using System.Windows.Forms.ComponentModel.Com2Interop;
using Windows.Win32.System.Com;
using Windows.Win32.System.Ole;
using Windows.Win32.System.Variant;

namespace System.Windows.Forms.Tests.ComponentModel.Com2Interop;

// NB: doesn't require thread affinity
public unsafe class COM2PictureConverterTests
{
    private Com2PictureConverter Instance { get; } = new(new Com2PropertyDescriptor(
        default,
        "Foo",
        default,
        default,
        default,
        default,
        default));

    [Fact]
    public void ConvertNativeToManaged_Null()
    {
        Assert.Null(Instance.ConvertNativeToManaged(default, null));
    }

    private unsafe class TestIPicture : IPictureMock
    {
        private readonly nint _handle;
        private readonly PICTYPE _type;

        public TestIPicture(nint handle, PICTYPE type = PICTYPE.PICTYPE_NONE)
        {
            _handle = handle;
            _type = type;
        }

        public override unsafe HRESULT get_Handle(OLE_HANDLE* pHandle)
        {
            if (pHandle is null)
            {
                return HRESULT.E_POINTER;
            }

            *pHandle = new((uint)(int)_handle);
            return HRESULT.S_OK;
        }

        public override unsafe HRESULT get_Type(PICTYPE* pType)
        {
            if (pType is null)
            {
                return HRESULT.E_POINTER;
            }

            *pType = _type;
            return HRESULT.S_OK;
        }
    }

    [Fact]
    public void ConvertNativeToManaged_NullHandle()
    {
        TestIPicture nullIPicture = new(0);
        using var unknown = ComHelpers.GetComScope<IUnknown>(nullIPicture);
        Assert.Null(Instance.ConvertNativeToManaged((VARIANT)unknown.Value, null));
    }

    [Fact]
    public unsafe void ConvertNativeToManaged_Icon()
    {
        Icon errorIcon = SystemIcons.Error;
        nint handle = errorIcon.Handle;
        TestIPicture iconIPicture = new(handle, PICTYPE.PICTYPE_ICON);
        using var unknown = ComHelpers.GetComScope<IUnknown>(iconIPicture);

        using Icon icon = (Icon)Instance.ConvertNativeToManaged((VARIANT)unknown.Value, null);

        Assert.Equal(icon.Height, errorIcon.Height);
        Assert.Equal(icon.Width, errorIcon.Width);
        Assert.Equal(typeof(Icon), Instance.ManagedType);

        // We should get the cached object if the handle didn't change
        Assert.Same(icon, (Icon)Instance.ConvertNativeToManaged((VARIANT)unknown.Value, null));
    }

    [Fact]
    public void ConvertNativeToManaged_Bitmap()
    {
        Icon errorIcon = SystemIcons.Error;
        using Bitmap errorBitmap = errorIcon.ToBitmap();
        nint hBitmap = errorBitmap.GetHbitmap();
        TestIPicture bitmapIPicture = new(hBitmap, PICTYPE.PICTYPE_BITMAP);
        try
        {
            using var unknown = ComHelpers.GetComScope<IUnknown>(bitmapIPicture);
            using Bitmap bitmap = (Bitmap)Instance.ConvertNativeToManaged((VARIANT)unknown.Value, property: null);

            Assert.Equal(bitmap.Height, errorIcon.Height);
            Assert.Equal(bitmap.Width, errorIcon.Width);
            Assert.Equal(typeof(Bitmap), Instance.ManagedType);

            // We should get the cached object if the handle didn't change
            Assert.Same(bitmap, (Bitmap)Instance.ConvertNativeToManaged((VARIANT)unknown.Value, property: null));
        }
        finally
        {
            PInvokeCore.DeleteObject((HGDIOBJ)hBitmap);
        }
    }

    [Fact]
    public void ConvertNativeToManaged_UnsupportedPICTYPE()
    {
        // The converter asserts, but doesn't throw. Suppress asserts so that we can validate it returns null as expected.
        using (new NoAssertContext())
        {
            using var unknown = ComHelpers.GetComScope<IUnknown>(new TestIPicture(1, PICTYPE.PICTYPE_METAFILE));
            Assert.Null(Instance.ConvertNativeToManaged((VARIANT)unknown.Value, null));
        }
    }

    [Fact]
    public void ConvertManagedToNative_NullObject()
    {
        bool cancelSet = true;
        Assert.True(Instance.ConvertManagedToNative(null, null, ref cancelSet).IsEmpty);
        Assert.False(cancelSet);
    }

    [StaFact]
    public unsafe void ConvertManagedToNative_Icon()
    {
        bool cancelSet = true;
        Icon exclamationIcon = SystemIcons.Exclamation;

        using VARIANT native = Instance.ConvertManagedToNative(exclamationIcon, null, ref cancelSet);
        using ComScope<IPicture> picture = ComScope<IPicture>.QueryFrom((IUnknown*)native);

        Assert.False(cancelSet);
        picture.Value->get_Height(out int height).ThrowOnFailure();
        picture.Value->get_Width(out int width).ThrowOnFailure();
        picture.Value->get_Type(out PICTYPE type).ThrowOnFailure();
        Assert.Equal(PICTYPE.PICTYPE_ICON, type);
        Assert.Equal(exclamationIcon.Height, GdiHelper.HimetricToPixelY(height));
        Assert.Equal(exclamationIcon.Width, GdiHelper.HimetricToPixelX(width));

        // And we should also round trip to the same value
        Assert.Same(exclamationIcon, Instance.ConvertNativeToManaged((VARIANT)picture.AsUnknown, null));
    }

    [StaFact]
    public unsafe void ConvertManagedToNative_Bitmap()
    {
        bool cancelSet = true;
        using Bitmap bitmap = new(42, 70);

        using VARIANT native = Instance.ConvertManagedToNative(bitmap, null, ref cancelSet);
        using ComScope<IPicture> picture = ComScope<IPicture>.QueryFrom((IUnknown*)native);

        Assert.False(cancelSet);
        picture.Value->get_Height(out int height).ThrowOnFailure();
        picture.Value->get_Width(out int width).ThrowOnFailure();
        picture.Value->get_Type(out PICTYPE type).ThrowOnFailure();
        Assert.Equal(PICTYPE.PICTYPE_BITMAP, type);
        Assert.Equal(bitmap.Height, GdiHelper.HimetricToPixelY(height));
        Assert.Equal(bitmap.Width, GdiHelper.HimetricToPixelX(width));

        // And we should also round trip to the same value
        Assert.Same(bitmap, Instance.ConvertNativeToManaged((VARIANT)picture.AsUnknown, null));
    }

    [Fact]
    public void ConvertManagedToNative_UnknownObjectType()
    {
        // The converter asserts, but doesn't throw. Suppress asserts so
        // that we can validate it returns null as expected.
        using (new NoAssertContext())
        {
            bool cancelSet = true;
            Assert.True(Instance.ConvertManagedToNative(new object(), null, ref cancelSet).IsEmpty);
            Assert.False(cancelSet);
        }
    }

    private unsafe class IPictureMock : IPicture.Interface, IManagedWrapper<IPicture>
    {
        public virtual HRESULT get_Handle(OLE_HANDLE* pHandle) => HRESULT.S_OK;
        public virtual HRESULT get_Type(PICTYPE* pType) => HRESULT.S_OK;
        public HRESULT get_Width(int* pWidth) => HRESULT.S_OK;
        public HRESULT get_Height(int* pHeight) => HRESULT.S_OK;
        public HRESULT get_CurDC(HDC* phDC) => HRESULT.S_OK;
        public HRESULT get_KeepOriginalFormat(BOOL* pKeep) => HRESULT.S_OK;
        public HRESULT put_KeepOriginalFormat(BOOL keep) => HRESULT.S_OK;
        public HRESULT get_Attributes(uint* pDwAttr) => HRESULT.S_OK;
        public HRESULT get_hPal(OLE_HANDLE* phPal) => HRESULT.S_OK;
        public HRESULT Render(HDC hDC, int x, int y, int cx, int cy, int xSrc, int ySrc, int cxSrc, int cySrc, RECT* pRcWBounds) => HRESULT.S_OK;
        public HRESULT set_hPal(OLE_HANDLE hPal) => HRESULT.S_OK;
        public HRESULT SelectPicture(HDC hDCIn, HDC* phDCOut, OLE_HANDLE* phBmpOut) => HRESULT.S_OK;
        public HRESULT PictureChanged() => HRESULT.S_OK;
        public HRESULT SaveAsFile(IStream* pStream, BOOL fSaveMemCopy, int* pCbSize) => HRESULT.S_OK;
    }
}
