using DeepDrftModels.Enums;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Microsoft.JSInterop.Infrastructure;

namespace DeepDrftTests;

/// <summary>
/// Unit tests for the Phase 16 share tracker (<see cref="ShareTracker"/>): the per-(target,channel)
/// debounce (§1b — at most one event per target+channel per 60s window per session). The tracker fires
/// through a beacon that wraps <see cref="IJSRuntime"/>; the tests use a no-op JS runtime (the send is
/// fire-and-forget and its outcome is irrelevant) and assert on the debounce decision via the bool the
/// recorder returns — true when an event fired, false when debounced.
/// </summary>
[TestFixture]
public class ShareTrackerTests
{
    // sendBeacon interop is fire-and-forget; the tracker never reads the result, so a no-op runtime that
    // returns default for any invocation is sufficient to exercise the debounce logic.
    private sealed class NoopJsRuntime : IJSRuntime
    {
        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
            => ValueTask.FromResult<TValue>(default!);

        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
            => ValueTask.FromResult<TValue>(default!);
    }

    // Minimal NavigationManager so the tracker can compose the (unused-in-test) beacon URL.
    private sealed class TestNavigationManager : NavigationManager
    {
        public TestNavigationManager() => Initialize("https://deepdrft.test/", "https://deepdrft.test/");
        protected override void NavigateToCore(string uri, bool forceLoad) { }
    }

    // Fixed-value anon-id provider so the tracker can attach a token without JS interop. Treated as
    // already warmed (Current returns the value); EnsureLoadedAsync is a no-op here.
    private sealed class StubAnonIdProvider : IAnonIdProvider
    {
        public StubAnonIdProvider(string? current) => Current = current;
        public string? Current { get; }
        public ValueTask EnsureLoadedAsync() => ValueTask.CompletedTask;
    }

    private ShareTracker _tracker = null!;
    private readonly DateTimeOffset _t0 = new(2026, 6, 19, 12, 0, 0, TimeSpan.Zero);

    [SetUp]
    public void SetUp()
        => _tracker = new ShareTracker(
            new BeaconInterop(new NoopJsRuntime()),
            new StubAnonIdProvider("anon-1"),
            new TestNavigationManager());

    // A copy-link records one share with channel = link.
    [Test]
    public void RecordShare_CopyLink_FiresOnce()
        => Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0), Is.True);

    // A copy-embed records one share with channel = embed — distinct (target,channel) from the link copy.
    [Test]
    public void RecordShare_CopyEmbedAfterLink_FiresSeparately()
    {
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0), Is.True);
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Embed, _t0), Is.True,
            "embed is a different channel from link — not debounced against it");
    }

    // An immediate repeat copy of the same (target, channel) within the window is debounced.
    [Test]
    public void RecordShare_ImmediateRepeat_IsDebounced()
    {
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0), Is.True);
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0.AddSeconds(5)), Is.False);
    }

    // After the 60s window elapses, the same (target, channel) fires again.
    [Test]
    public void RecordShare_AfterWindow_FiresAgain()
    {
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0), Is.True);
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "k", ShareChannel.Link, _t0.AddSeconds(61)), Is.True);
    }

    // Different targets debounce independently — sharing track A then track B both fire.
    [Test]
    public void RecordShare_DifferentTargets_FireIndependently()
    {
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "a", ShareChannel.Link, _t0), Is.True);
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "b", ShareChannel.Link, _t0), Is.True);
    }

    // A track key and a release key are distinct targets even if the key string collides.
    [Test]
    public void RecordShare_TrackVsRelease_AreDistinctTargets()
    {
        Assert.That(_tracker.RecordShare(ShareTargetType.Track, "x", ShareChannel.Link, _t0), Is.True);
        Assert.That(_tracker.RecordShare(ShareTargetType.Release, "x", ShareChannel.Link, _t0), Is.True);
    }

    // A blank target key never fires (defensive — the popover guards too).
    [Test]
    public void RecordShare_BlankKey_DoesNotFire()
        => Assert.That(_tracker.RecordShare(ShareTargetType.Track, "  ", ShareChannel.Link, _t0), Is.False);
}
