using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;

/// <summary>
///     v3 {{equip}}/&lt;RefName&gt; editor niceties: hover on a path literal containing the token shows an
///     "equipment-relative" note (not the "not a known configured tag path" warning); completion after
///     <c>{{equip}}/</c> offers the owning equipment's reference effective names; and the diagnostic flags an
///     unresolved <c>{{equip}}/&lt;RefName&gt;</c> exactly as the deploy gate does (editor accepts ⇔ publish
///     accepts).
/// </summary>
public sealed class EquipTokenEditorTests
{
    /// <summary>A fake catalog whose equipment has two references — "Speed" and "Temp" — so those are the
    /// resolvable {{equip}}/&lt;RefName&gt; names for the completion + diagnostic.</summary>
    private sealed class FakeCatalog : IScriptTagCatalog
    {
        public Task<IReadOnlyList<string>> GetPathsAsync(string? filter, CancellationToken ct)
            => Task.FromResult<IReadOnlyList<string>>(new[] { "Cell1/Modbus/Dev1/Speed" });

        public Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct)
            => Task.FromResult<ScriptTagInfo?>(null);

        public Task<IReadOnlyList<string>> GetEquipmentReferenceNamesAsync(string equipmentId, string? filter, CancellationToken ct)
        {
            IEnumerable<string> names = new[] { "Speed", "Temp" };
            if (!string.IsNullOrWhiteSpace(filter))
                names = names.Where(n => n.StartsWith(filter, StringComparison.OrdinalIgnoreCase));
            return Task.FromResult<IReadOnlyList<string>>(names.ToList());
        }
    }

    private static readonly ScriptAnalysisService Svc = new(new FakeCatalog());

    private static async Task<IReadOnlyList<CompletionItem>> Items(string code, int line, int col, string? equipmentId)
        => (await Svc.CompleteAsync(new CompletionsRequest(code, line, col, equipmentId))).Items;

    [Fact] public async Task Completion_after_equip_slash_offers_reference_names()
    {
        // caret right after the slash of "{{equip}}/" — column 30 sits between the trailing slash and the
        // closing quote of ctx.GetTag("{{equip}}/").
        var items = await Items("""return ctx.GetTag("{{equip}}/").Value;""", 1, 30, "EQ-1");
        items.Select(i => i.Label).ShouldContain("{{equip}}/Speed");
        items.Select(i => i.Label).ShouldContain("{{equip}}/Temp");
        items.ShouldAllBe(i => i.Detail == "tag path");
    }

    [Fact] public async Task Completion_after_equip_slash_inserts_the_full_token_qualified_ref()
    {
        var items = await Items("""return ctx.GetTag("{{equip}}/").Value;""", 1, 30, "EQ-1");
        var speed = items.FirstOrDefault(i => i.Label == "{{equip}}/Speed");
        speed.ShouldNotBeNull();
        speed!.InsertText.ShouldBe("{{equip}}/Speed");
    }

    [Fact] public async Task Completion_after_equip_slash_is_inert_without_equipment_context()
    {
        // Shared ScriptEdit page (no EquipmentId) can't resolve references → no equip completions.
        var items = await Items("""return ctx.GetTag("{{equip}}/").Value;""", 1, 30, null);
        items.ShouldBeEmpty();
    }

    [Fact] public async Task Hover_on_equip_token_literal_notes_equipment_relative()
    {
        // caret inside ctx.GetTag("{{equip}}/Speed")
        var md = (await Svc.Hover(new HoverRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", 1, 24))).Markdown;
        md.ShouldNotBeNull();
        md!.ShouldContain("Equipment-relative");
        // the {{equip}} token must render literally (non-interpolated markdown segment)
        md.ShouldContain("{{equip}}");
        md.ShouldNotContain("Not a known");
    }

    [Fact] public async Task Diagnostic_flags_unresolved_equip_reference()
    {
        // "Missing" is not one of the equipment's references (Speed/Temp) → a diagnostic, matching the
        // deploy gate. Uses the equipment context via the request's EquipmentId.
        var resp = await Svc.DiagnoseAsync(
            new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;""", "EQ-1"));
        resp.Markers.ShouldContain(m => m.Code == "OTSCRIPT_EQUIPREF" && m.Message.Contains("Missing"));
    }

    [Fact] public async Task Diagnostic_clean_for_resolved_equip_reference()
    {
        var resp = await Svc.DiagnoseAsync(
            new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", "EQ-1"));
        resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF");
    }

    [Fact] public async Task Diagnostic_equipref_inert_without_equipment_context()
    {
        // No EquipmentId (shared ScriptEdit page) → the equip-ref check is inert (base markers only),
        // matching the deploy gate which only resolves per owning equipment.
        var resp = await Svc.DiagnoseAsync(
            new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;"""));
        resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF");
    }
}
