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

using System.Drawing;
using Windows.Win32.UI.Controls.RichEdit;
using static Interop;

namespace System.Windows.Forms.UITests;

public class RichTextBoxTests : ControlTestBase
{
    public RichTextBoxTests(ITestOutputHelper testOutputHelper)
        : base(testOutputHelper)
    {
    }

    [WinFormsFact]
    public async Task RichTextBox_Click_On_Friendly_Name_Link_Provides_Hidden_Link_SpanAsync()
    {
        await RunTestAsync(async (form, richTextBox) =>
        {
            richTextBox.Rtf = @"{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang4105{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\*\generator Riched20 10.0.17134}\viewkind4\uc1 
{\field{\*\fldinst { HYPERLINK ""http://www.google.com"" }}{\fldrslt {Click link #1}}}
\pard\sa200\sl276\slmult1\f0\fs22\lang9  for more information.\par
{\field{\*\fldinst { HYPERLINK ""http://www.google.com"" }}{\fldrslt {Click link #2}}}
\pard\sa200\sl276\slmult1\f0\fs22\lang9  for more information.\par
{\field{\*\fldinst { HYPERLINK ""http://www.google.com"" }}{\fldrslt {Click link #3}}}
\pard\sa200\sl276\slmult1\f0\fs22\lang9  for more information.\par
}";

            LinkClickedEventArgs? result = null;
            LinkClickedEventHandler handler = (sender, e) => result = e;
            richTextBox.LinkClicked += handler;
            try
            {
                Point pt = richTextBox.PointToScreen(richTextBox.GetPositionFromCharIndex(richTextBox.Text.IndexOf("Click link #2", StringComparison.Ordinal)));

                // Adjust point a bit to make sure we are clicking inside the character cell instead of on its edge.
                pt.X += 2;
                pt.Y += 2;
                await MoveMouseAsync(form, pt);
                await InputSimulator.SendAsync(
                    form,
                    inputSimulator => inputSimulator.Mouse.LeftButtonClick());
            }
            finally
            {
                richTextBox.LinkClicked -= handler;
            }

            Assert.NotNull(result);

            Assert.True(result!.LinkStart + result.LinkLength <= richTextBox.Text.Length);
            Assert.Equal(result.LinkText, richTextBox.Text.Substring(result.LinkStart, result.LinkLength));

            // This assumes the input span is the hidden text of a "friendly name" URL,
            // which is what the native control will pass to the LinkClicked event instead
            // of the actual span of the clicked display text.
            string? displayText = GetTextFromRange(richTextBox, result.LinkStart, result.LinkLength, range =>
            {
                unsafe
                {
                    // Move the cursor to the end of the hidden area we are currently located in.
                    range.Value->EndOf((int)tomConstants.tomHidden, 0, out int _).ThrowOnFailure();

                    // Extend the cursor to the end of the display text of the link.
                    range.Value->EndOf((int)tomConstants.tomLink, 1, out int _).ThrowOnFailure();
                }
            });

            Assert.Equal("Click link #2", displayText);
        });
    }

    [WinFormsFact]
    public async Task RichTextBox_Click_On_Custom_Link_Preceeded_By_Hidden_Text_Provides_Displayed_Link_SpanAsync()
    {
        await RunTestAsync(async (form, richTextBox) =>
        {
            richTextBox.Rtf = @"{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang4105{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\*\generator Riched20 10.0.17134}\viewkind4\uc1\pard\sa200\sl276\slmult1\f0\fs22\lang9
This is hidden text preceeding a \v #link1#\v0 custom link.\par
This is hidden text preceeding a \v #link2#\v0 custom link.\par
This is hidden text preceeding a \v #link3#\v0 custom link.\par
}";

            MakeLink(richTextBox, "#link1#custom link");
            MakeLink(richTextBox, "#link2#custom link");
            MakeLink(richTextBox, "#link3#custom link");

            LinkClickedEventArgs? result = null;
            LinkClickedEventHandler handler = (sender, e) => result = e;
            richTextBox.LinkClicked += handler;
            try
            {
                Point pt = richTextBox.PointToScreen(richTextBox.GetPositionFromCharIndex(richTextBox.Text.IndexOf("#link2#custom link", StringComparison.Ordinal)));

                // Adjust point a bit to make sure we are clicking inside the character cell instead of on its edge.
                pt.X += 2;
                pt.Y += 2;
                await MoveMouseAsync(form, pt);
                await InputSimulator.SendAsync(
                    form,
                    inputSimulator => inputSimulator.Mouse.LeftButtonClick());
            }
            finally
            {
                richTextBox.LinkClicked -= handler;
            }

            Assert.NotNull(result);

            Assert.True(result!.LinkStart + result.LinkLength <= richTextBox.Text.Length);
            Assert.Equal(result.LinkText, richTextBox.Text.Substring(result.LinkStart, result.LinkLength));

            // This assumes the input span is a custom link preceeded by hidden text.
            string? hiddenText = GetTextFromRange(richTextBox, result.LinkStart, result.LinkLength, range =>
            {
                unsafe
                {
                    // Move the cursor to the start of the link we are currently located in.
                    range.Value->StartOf((int)tomConstants.tomLink, 0, out int _).ThrowOnFailure();

                    // Extend the cursor to the start of the hidden area preceeding the link.
                    range.Value->StartOf((int)tomConstants.tomHidden, 1, out int _).ThrowOnFailure();
                }
            });

            Assert.Equal("#link2#", hiddenText);
        });
    }

    [WinFormsFact]
    public async Task RichTextBox_Click_On_Custom_Link_Followed_By_Hidden_Text_Provides_Displayed_Link_SpanAsync()
    {
        await RunTestAsync(async (form, richTextBox) =>
        {
            // This needs to be sufficiently different from the previous test so we don't click on the same location twice,
            // otherwise the tests may execute fast enough for the second test to register as a double click.
            richTextBox.Rtf = @"{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang4105{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
        {\*\generator Riched20 10.0.17134}\viewkind4\uc1\pard\sa200\sl276\slmult1\f0\fs22\lang9
        This is a custom link\v #link1#\v0  which is followed by hidden text.\par
        This is a custom link\v #link2#\v0  which is followed by hidden text.\par
        This is a custom link\v #link3#\v0  which is followed by hidden text.\par
        }";

            MakeLink(richTextBox, "custom link#link1#");
            MakeLink(richTextBox, "custom link#link2#");
            MakeLink(richTextBox, "custom link#link3#");

            LinkClickedEventArgs? result = null;
            LinkClickedEventHandler handler = (sender, e) => result = e;
            richTextBox.LinkClicked += handler;
            try
            {
                Point pt = richTextBox.PointToScreen(richTextBox.GetPositionFromCharIndex(
                    richTextBox.Text.IndexOf("custom link#link2#", StringComparison.Ordinal)));

                // Adjust point a bit to make sure we are clicking inside the character cell instead of on its edge.
                pt.X += 2;
                pt.Y += 2;
                await MoveMouseAsync(form, pt);
                await InputSimulator.SendAsync(
                    form,
                    inputSimulator => inputSimulator.Mouse.LeftButtonClick());
            }
            finally
            {
                richTextBox.LinkClicked -= handler;
            }

            Assert.NotNull(result);

            Assert.True(result!.LinkStart + result.LinkLength <= richTextBox.Text.Length);
            Assert.Equal(result.LinkText, richTextBox.Text.Substring(result.LinkStart, result.LinkLength));

            // This assumes the input span is a custom link followed by hidden text.
            string? hiddenText = GetTextFromRange(richTextBox, result.LinkStart, result.LinkLength, range =>
            {
                unsafe
                {
                    // Move the cursor to the end of link we are currently located in.
                    range.Value->EndOf((int)tomConstants.tomLink, 0, out int _).ThrowOnFailure();

                    // Extend the cursor to the end of the hidden area following the link.
                    range.Value->EndOf((int)tomConstants.tomHidden, 1, out int _).ThrowOnFailure();
                }
            });

            Assert.Equal("#link2#", hiddenText);
        });
    }

    private unsafe void MakeLink(RichTextBox control, string text)
    {
        control.Select(control.Text.IndexOf(text, StringComparison.Ordinal), text.Length);

        var format = new Richedit.CHARFORMAT2W
        {
            cbSize = (uint)sizeof(Richedit.CHARFORMAT2W),
            dwMask = CFM_MASK.CFM_LINK,
            dwEffects = CFE_EFFECTS.CFE_LINK,
        };

        PInvokeCore.SendMessage(control, PInvokeCore.EM_SETCHARFORMAT, (WPARAM)PInvoke.SCF_SELECTION, ref format);

        control.Select(0, 0);
    }

    private unsafe string? GetTextFromRange(RichTextBox control, int start, int length, Action<Pointer<ITextRange>>? transform)
    {
        using ComScope<IRichEditOle> richEdit = new(null);

        if (PInvokeCore.SendMessage(control, PInvokeCore.EM_GETOLEINTERFACE, 0, (void**)richEdit) != 0)
        {
            using var textDocument = richEdit.TryQuery<ITextDocument>(out HRESULT hr);

            if (hr.Succeeded)
            {
                using ComScope<ITextRange> range = new(null);
                textDocument.Value->Range(start, start + length, range).ThrowOnFailure();
                transform?.Invoke((ITextRange*)range);
                using BSTR text = default;
                range.Value->GetText(&text).ThrowOnFailure();
                return text.ToString();
            }
        }

        return null;
    }

    private async Task RunTestAsync(Func<Form, RichTextBox, Task> runTest)
    {
        await RunSingleControlTestAsync(
            testDriverAsync: runTest,
            createControl: () =>
            {
                RichTextBox control = new()
                {
                    Size = new Size(439, 103),
                    DetectUrls = false,
                };

                return control;
            },
            createForm: () =>
            {
                return new()
                {
                    Size = new(300, 300),
                    Location = new Point(100, 100),
                };
            });
    }
}
