using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
using HellionChat.Themes;

namespace HellionChat.SelfTests;

// Verifies the v1.5.4 PM-1 crossfade contract: switching the active
// theme arms TryGetActiveCrossfade for ~300ms, then the registry
// returns to direct AbgrCache reads. A second switch within 100ms
// keeps the lerped path active (no identity-snap). CleanUp restores
// the initial theme so /xlperf stays idempotent.
internal sealed class ThemeCrossfadeSelfTestStep : ISelfTestStep
{
    private readonly Plugin plugin;

    private string? initialSlug;
    private string? targetSlug;
    private string? midSwitchSlug;
    private long armedAtTickMs = long.MinValue;
    private long midArmedAtTickMs = long.MinValue;
    private bool sawCrossfade;
    private bool sawMidCrossfadeSwitch;
    private bool sawCrossfadeEnd;
    private bool restoredInitial;

    public ThemeCrossfadeSelfTestStep(Plugin plugin)
    {
        this.plugin = plugin;
    }

    public string Name => "Hellion Chat - Theme crossfade";

    public SelfTestStepResult RunStep()
    {
        var registry = this.plugin.ThemeRegistry;
        if (registry is null)
            return SelfTestStepResult.Fail;

        if (this.initialSlug is null)
        {
            this.initialSlug = registry.Active.Slug;
            this.targetSlug = PickDifferentSlug(registry, this.initialSlug);
            if (this.targetSlug is null)
            {
                ImGui.Text("Need at least two themes available; only one built-in found.");
                return SelfTestStepResult.Fail;
            }

            registry.Switch(this.targetSlug);
            this.armedAtTickMs = Environment.TickCount64;
            ImGui.Text($"Crossfade armed: {this.initialSlug} -> {this.targetSlug}");
            return SelfTestStepResult.Waiting;
        }

        if (!this.sawCrossfade)
        {
            if (registry.TryGetActiveCrossfade(out _))
            {
                this.sawCrossfade = true;
                this.midArmedAtTickMs = Environment.TickCount64;
                ImGui.Text("Crossfade observed mid-window, arming mid-switch test...");
                return SelfTestStepResult.Waiting;
            }

            // If the window already closed before we observed it, that
            // is acceptable only on extremely slow frame paths; accept
            // it as "saw the start" if more than 300ms have elapsed.
            // Skip the mid-crossfade-switch phase in that case -- the
            // lerped path is no longer active, so a second switch would
            // re-arm a fresh crossfade and not exercise PM-1b's
            // mid-flight-origin override.
            if (Environment.TickCount64 - this.armedAtTickMs > 300)
            {
                this.sawCrossfade = true;
                this.sawMidCrossfadeSwitch = true;
                this.sawCrossfadeEnd = true;
                ImGui.Text("Crossfade window closed before observation; accepting.");
                return SelfTestStepResult.Waiting;
            }

            return SelfTestStepResult.Waiting;
        }

        if (!this.sawMidCrossfadeSwitch)
        {
            // PM-Test-3 mid-crossfade-switch phase: within ~100ms of the
            // first observed crossfade, fire a second Switch to a THIRD
            // theme. ArmCrossfade must compose the current lerped state
            // as the new origin -- TryGetActiveCrossfade still returns
            // true (lerped path stays active, no identity-snap) and the
            // lerped value is neither the identity-from nor the
            // identity-to of the new switch (origin shifted to the
            // mid-flight cache, target is the third theme).
            if (Environment.TickCount64 - this.midArmedAtTickMs < 100)
            {
                this.midSwitchSlug = PickDifferentSlug(
                    registry,
                    [this.initialSlug!, this.targetSlug!]
                );
                if (this.midSwitchSlug is null)
                {
                    // Only two themes available -- mid-switch phase cannot
                    // exercise the lerped-origin path. Accept and move on
                    // (the v1.5.3 baseline ships >=10 built-ins, so this
                    // branch is defensive).
                    this.sawMidCrossfadeSwitch = true;
                    ImGui.Text("Only two themes available; skipping mid-switch assert.");
                    return SelfTestStepResult.Waiting;
                }

                var fromCache = registry.Active.AbgrCache;
                registry.Switch(this.midSwitchSlug);
                var toCache = registry.Active.AbgrCache;

                if (!registry.TryGetActiveCrossfade(out var midLerped))
                {
                    ImGui.Text("Mid-switch failed: TryGetActiveCrossfade returned false.");
                    return SelfTestStepResult.Fail;
                }

                // Lerped value must be neither the new identity-from
                // (target cache of the first switch) nor the new
                // identity-to (third theme cache) -- it must originate
                // from the mid-flight composed snapshot.
                if (midLerped.Equals(fromCache) || midLerped.Equals(toCache))
                {
                    ImGui.Text("Mid-switch failed: lerped value is an identity snap.");
                    return SelfTestStepResult.Fail;
                }

                this.sawMidCrossfadeSwitch = true;
                ImGui.Text(
                    $"Mid-switch armed: {this.targetSlug} -> {this.midSwitchSlug} (lerped origin)."
                );
                return SelfTestStepResult.Waiting;
            }

            // Window for mid-switch already elapsed; accept and continue.
            this.sawMidCrossfadeSwitch = true;
            ImGui.Text("Mid-switch window elapsed before fire; accepting.");
            return SelfTestStepResult.Waiting;
        }

        if (!this.sawCrossfadeEnd)
        {
            if (!registry.TryGetActiveCrossfade(out _))
            {
                this.sawCrossfadeEnd = true;
                ImGui.Text("Crossfade window closed cleanly.");
                return SelfTestStepResult.Waiting;
            }
            return SelfTestStepResult.Waiting;
        }

        if (!this.restoredInitial)
        {
            registry.Switch(this.initialSlug);
            this.restoredInitial = true;
            ImGui.Text($"Restored: {this.initialSlug}");
            return SelfTestStepResult.Waiting;
        }

        // Wait for the restore-crossfade to also conclude before
        // declaring Pass, so /xlperf does not flicker out mid-fade.
        if (registry.TryGetActiveCrossfade(out _))
            return SelfTestStepResult.Waiting;

        return SelfTestStepResult.Pass;
    }

    public void CleanUp()
    {
        // Best-effort: if anything went sideways, snap back to the
        // initial slug. Switch is idempotent on same-slug.
        var registry = this.plugin.ThemeRegistry;
        if (registry is not null && this.initialSlug is not null)
        {
            registry.Switch(this.initialSlug);
        }

        this.initialSlug = null;
        this.targetSlug = null;
        this.midSwitchSlug = null;
        this.armedAtTickMs = long.MinValue;
        this.midArmedAtTickMs = long.MinValue;
        this.sawCrossfade = false;
        this.sawMidCrossfadeSwitch = false;
        this.sawCrossfadeEnd = false;
        this.restoredInitial = false;
    }

    private static string? PickDifferentSlug(ThemeRegistry registry, string activeSlug) =>
        PickDifferentSlug(registry, [activeSlug]);

    private static string? PickDifferentSlug(
        ThemeRegistry registry,
        IReadOnlyCollection<string> excludeSlugs
    )
    {
        foreach (var theme in registry.AllBuiltIns())
        {
            var match = false;
            foreach (var excluded in excludeSlugs)
            {
                if (string.Equals(theme.Slug, excluded, StringComparison.OrdinalIgnoreCase))
                {
                    match = true;
                    break;
                }
            }
            if (!match)
                return theme.Slug;
        }
        return null;
    }
}
