using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;

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

/// <summary>Unit tests for <see cref="BrowserSessionService"/> — driver-type dispatch
/// on open, NotFound semantics on unknown tokens, exception swallowing, per-call
/// timeout enforcement, and the DriverOperator gate on the one write-capable member.</summary>
public sealed class BrowserSessionServiceTests
{
    private static BrowserSessionService NewService(
        BrowseSessionRegistry registry, params IDriverBrowser[] browsers) =>
        NewService(registry, new FakeUniversalDriverBrowser(), browsers);

    private static BrowserSessionService NewService(
        BrowseSessionRegistry registry, IUniversalDriverBrowser universal, params IDriverBrowser[] browsers) =>
        new(browsers, registry, NullLogger<BrowserSessionService>.Instance, universal,
            new FakeAuthorizationService(succeeds: true), new FakeAuthenticationStateProvider());

    private static BrowserSessionService NewServiceWithAuthz(
        BrowseSessionRegistry registry, bool authorized) =>
        new([], registry, NullLogger<BrowserSessionService>.Instance, new FakeUniversalDriverBrowser(),
            new FakeAuthorizationService(authorized), new FakeAuthenticationStateProvider());

    [Fact]
    public async Task OpenAsync_unknown_driver_type_returns_Ok_false_with_message()
    {
        var registry = new BrowseSessionRegistry();
        var service = NewService(registry, new FakeDriverBrowser("Known"));

        var result = await service.OpenAsync("Unknown", "{}", CancellationToken.None);

        result.Ok.ShouldBeFalse();
        result.Token.ShouldBe(Guid.Empty);
        result.Message.ShouldNotBeNull();
        result.Message!.ShouldContain("Unknown");
    }

    [Fact]
    public async Task OpenAsync_happy_path_returns_token_and_registers()
    {
        var registry = new BrowseSessionRegistry();
        var session = new FakeBrowseSession();
        var browser = new FakeDriverBrowser("Galaxy")
        {
            OpenHandler = (_, _) => Task.FromResult<IBrowseSession>(session),
        };
        var service = NewService(registry, browser);

        var result = await service.OpenAsync("Galaxy", "{}", CancellationToken.None);

        result.Ok.ShouldBeTrue();
        result.Message.ShouldBeNull();
        result.Token.ShouldNotBe(Guid.Empty);
        result.Token.ShouldBe(session.Token);
        registry.TryGet(result.Token, out var registered).ShouldBeTrue();
        registered.ShouldBeSameAs((IBrowseSession)session);
    }

    [Fact]
    public async Task OpenAsync_swallows_driver_throws_returns_Ok_false()
    {
        var registry = new BrowseSessionRegistry();
        var browser = new FakeDriverBrowser("Galaxy")
        {
            OpenHandler = (_, _) => throw new InvalidOperationException("boom"),
        };
        var service = NewService(registry, browser);

        var result = await service.OpenAsync("Galaxy", "{}", CancellationToken.None);

        result.Ok.ShouldBeFalse();
        result.Token.ShouldBe(Guid.Empty);
        result.Message.ShouldNotBeNull();
        result.Message!.ShouldContain("boom");
    }

    [Fact]
    public async Task Open_BespokeBrowserWinsOverUniversal()
    {
        var registry = new BrowseSessionRegistry();
        var bespokeSession = new FakeBrowseSession();
        var bespoke = new FakeDriverBrowser("AbCip")
        {
            OpenHandler = (_, _) => Task.FromResult<IBrowseSession>(bespokeSession),
        };
        var universal = new FakeUniversalDriverBrowser
        {
            CanBrowsePredicate = (_, _) => true, // universal would also serve it
            OpenHandler = (_, _, _) => Task.FromResult<IBrowseSession>(new FakeBrowseSession()),
        };
        var service = NewService(registry, universal, bespoke);

        var result = await service.OpenAsync("AbCip", "{}", CancellationToken.None);

        result.Ok.ShouldBeTrue();
        result.Token.ShouldBe(bespokeSession.Token); // bespoke session, not the universal one
    }

    [Fact]
    public async Task Open_FallsBackToUniversal_WhenNoBespokeAndCanBrowse()
    {
        var registry = new BrowseSessionRegistry();
        var universalSession = new FakeBrowseSession();
        var universal = new FakeUniversalDriverBrowser
        {
            CanBrowsePredicate = (t, _) => t == "AbCip",
            OpenHandler = (_, _, _) => Task.FromResult<IBrowseSession>(universalSession),
        };
        var service = NewService(registry, universal); // no bespoke browsers

        var result = await service.OpenAsync("AbCip", "{}", CancellationToken.None);

        result.Ok.ShouldBeTrue();
        result.Token.ShouldBe(universalSession.Token);
        registry.TryGet(result.Token, out _).ShouldBeTrue();
    }

    [Fact]
    public async Task Open_NoBespokeAndCannotBrowse_ReturnsNoBrowserMessage()
    {
        var registry = new BrowseSessionRegistry();
        var service = NewService(registry, new FakeUniversalDriverBrowser()); // CanBrowse=false

        var result = await service.OpenAsync("Modbus", "{}", CancellationToken.None);

        result.Ok.ShouldBeFalse();
        result.Token.ShouldBe(Guid.Empty);
        result.Message!.ShouldContain("Modbus");
    }

    [Fact]
    public async Task Open_UniversalThrow_ReturnsOkFalseWithMessage()
    {
        var registry = new BrowseSessionRegistry();
        var universal = new FakeUniversalDriverBrowser
        {
            CanBrowsePredicate = (_, _) => true,
            OpenHandler = (_, _, _) => throw new InvalidOperationException("universal-boom"),
        };
        var service = NewService(registry, universal);

        var result = await service.OpenAsync("AbCip", "{}", CancellationToken.None);

        result.Ok.ShouldBeFalse();
        result.Message!.ShouldContain("universal-boom");
    }

    [Fact]
    public void CanBrowse_TrueForBespokeType_TrueForUniversalType_FalseOtherwise()
    {
        var registry = new BrowseSessionRegistry();
        var universal = new FakeUniversalDriverBrowser { CanBrowsePredicate = (t, _) => t == "AbCip" };
        var service = NewService(registry, universal, new FakeDriverBrowser("Galaxy"));

        service.CanBrowse("Galaxy", "{}").ShouldBeTrue(); // bespoke registered
        service.CanBrowse("AbCip", "{}").ShouldBeTrue();  // universal can serve
        service.CanBrowse("Modbus", "{}").ShouldBeFalse(); // neither
    }

    [Fact]
    public async Task RootAsync_unknown_token_throws_BrowseSessionNotFoundException()
    {
        var registry = new BrowseSessionRegistry();
        var service = NewService(registry);

        await Should.ThrowAsync<BrowseSessionNotFoundException>(
            () => service.RootAsync(Guid.NewGuid(), CancellationToken.None));
    }

    [Fact]
    public async Task RootAsync_invokes_session_Root()
    {
        var registry = new BrowseSessionRegistry();
        IReadOnlyList<BrowseNode> expected = new[]
        {
            new BrowseNode("ns=2;s=A", "A", BrowseNodeKind.Folder, true),
            new BrowseNode("ns=2;s=B", "B", BrowseNodeKind.Leaf, false),
        };
        var session = new FakeBrowseSession { RootHandler = _ => Task.FromResult(expected) };
        registry.Register(session);
        var service = NewService(registry);

        var actual = await service.RootAsync(session.Token, CancellationToken.None);

        actual.ShouldBe(expected);
    }

    [Fact]
    public async Task RootAsync_enforces_PerCallTimeout()
    {
        var registry = new BrowseSessionRegistry();
        var session = new FakeBrowseSession
        {
            // Awaits the linked CTS — the service must cancel it via PerCallTimeout (20s).
            RootHandler = ct => Task.Delay(TimeSpan.FromSeconds(40), ct)
                .ContinueWith<IReadOnlyList<BrowseNode>>(_ => Array.Empty<BrowseNode>(), ct),
        };
        registry.Register(session);
        var service = NewService(registry);

        var sw = System.Diagnostics.Stopwatch.StartNew();
        await Should.ThrowAsync<OperationCanceledException>(
            () => service.RootAsync(session.Token, CancellationToken.None));
        sw.Stop();

        // Real timeout is 20s; allow generous slack but cap well below the 40s task delay.
        sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(35));
    }

    [Fact]
    public async Task CloseAsync_removes_and_disposes_session()
    {
        var registry = new BrowseSessionRegistry();
        var session = new FakeBrowseSession();
        var browser = new FakeDriverBrowser("Galaxy")
        {
            OpenHandler = (_, _) => Task.FromResult<IBrowseSession>(session),
        };
        var service = NewService(registry, browser);
        var opened = await service.OpenAsync("Galaxy", "{}", CancellationToken.None);
        opened.Ok.ShouldBeTrue();

        await service.CloseAsync(opened.Token);

        registry.TryGet(opened.Token, out _).ShouldBeFalse();
        session.Disposed.ShouldBeTrue();

        // Unknown token is a no-op.
        await Should.NotThrowAsync(() => service.CloseAsync(Guid.NewGuid()));
    }

    // ---------------------------------------------------- RequestRebirthAsync: the one write path

    [Fact]
    public async Task RequestRebirthAsync_WithoutDriverOperator_IsRefusedAndNeverReachesTheSession()
    {
        // A publishing action that skips authz is a security bug, not a style issue: this one makes a
        // live plant broker receive a command. The refusal must happen BEFORE the session is touched.
        var registry = new BrowseSessionRegistry();
        var session = new FakeRebirthCapableBrowseSession();
        registry.Register(session);
        var service = NewServiceWithAuthz(registry, authorized: false);

        await Should.ThrowAsync<UnauthorizedAccessException>(
            () => service.RequestRebirthAsync(session.Token, "Plant1/EdgeA", CancellationToken.None));

        session.Scopes.ShouldBeEmpty();
    }

    [Fact]
    public async Task RequestRebirthAsync_WithDriverOperator_DispatchesTheScopeAndReturnsTheCount()
    {
        var registry = new BrowseSessionRegistry();
        var session = new FakeRebirthCapableBrowseSession { Published = 2 };
        registry.Register(session);
        var service = NewServiceWithAuthz(registry, authorized: true);

        var published = await service.RequestRebirthAsync(session.Token, "Plant1", CancellationToken.None);

        published.ShouldBe(2);
        session.Scopes.ShouldBe(["Plant1"]);
    }

    [Fact]
    public async Task RequestRebirthAsync_OnANonRebirthCapableSession_IsNotSupported()
    {
        var registry = new BrowseSessionRegistry();
        var session = new FakeBrowseSession();
        registry.Register(session);
        var service = NewServiceWithAuthz(registry, authorized: true);

        await Should.ThrowAsync<NotSupportedException>(
            () => service.RequestRebirthAsync(session.Token, "Plant1/EdgeA", CancellationToken.None));
    }

    [Fact]
    public async Task RequestRebirthAsync_UnknownToken_ThrowsBrowseSessionNotFound()
    {
        var registry = new BrowseSessionRegistry();
        var service = NewServiceWithAuthz(registry, authorized: true);

        await Should.ThrowAsync<BrowseSessionNotFoundException>(
            () => service.RequestRebirthAsync(Guid.NewGuid(), "Plant1/EdgeA", CancellationToken.None));
    }

    [Fact]
    public void CanRequestRebirth_IsTrue_OnlyForASessionThatAdvertisesTheAction()
    {
        // The picker draws its Request-rebirth affordance off this. A session type CAN re-announce
        // (MqttBrowseSession implements the interface) while a given INSTANCE cannot — a plain-MQTT
        // window publishes nothing, ever — so a type test alone would draw a button that only throws.
        var registry = new BrowseSessionRegistry();
        var sparkplug = new FakeRebirthCapableBrowseSession();
        var plain = new FakeRebirthCapableBrowseSession { RebirthAvailable = false };
        var plainOldSession = new FakeBrowseSession();
        registry.Register(sparkplug);
        registry.Register(plain);
        registry.Register(plainOldSession);
        var service = NewServiceWithAuthz(registry, authorized: true);

        service.CanRequestRebirth(sparkplug.Token).ShouldBeTrue();
        service.CanRequestRebirth(plain.Token).ShouldBeFalse();
        service.CanRequestRebirth(plainOldSession.Token).ShouldBeFalse();
        service.CanRequestRebirth(Guid.NewGuid()).ShouldBeFalse();   // unknown/reaped token
    }

    /// <summary>A browse session that records the rebirth scopes it was asked for.</summary>
    private sealed class FakeRebirthCapableBrowseSession : IRebirthCapableBrowseSession
    {
        public Guid Token { get; } = Guid.NewGuid();

        public DateTime LastUsedUtc { get; } = DateTime.UtcNow;

        public int Published { get; init; } = 1;

        /// <summary>Whether this fake session advertises the action (a plain-MQTT window does not).</summary>
        public bool RebirthAvailable { get; init; } = true;

        public List<string> Scopes { get; } = [];

        public Task<int> RequestRebirthAsync(string scope, CancellationToken cancellationToken)
        {
            Scopes.Add(scope);
            return Task.FromResult(Published);
        }

        public Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken) =>
            Task.FromResult<IReadOnlyList<BrowseNode>>([]);

        public Task<IReadOnlyList<BrowseNode>> ExpandAsync(string nodeId, CancellationToken cancellationToken) =>
            Task.FromResult<IReadOnlyList<BrowseNode>>([]);

        public Task<IReadOnlyList<AttributeInfo>> AttributesAsync(string nodeId, CancellationToken cancellationToken) =>
            Task.FromResult<IReadOnlyList<AttributeInfo>>([]);

        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }

    /// <summary>An <see cref="IAuthorizationService"/> whose verdict the test dictates.</summary>
    private sealed class FakeAuthorizationService(bool succeeds) : IAuthorizationService
    {
        public Task<AuthorizationResult> AuthorizeAsync(
            ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements) =>
            Task.FromResult(succeeds ? AuthorizationResult.Success() : AuthorizationResult.Failed());

        public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName) =>
            Task.FromResult(succeeds ? AuthorizationResult.Success() : AuthorizationResult.Failed());
    }

    /// <summary>A fixed authenticated identity for the circuit under test.</summary>
    private sealed class FakeAuthenticationStateProvider : AuthenticationStateProvider
    {
        public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
            Task.FromResult(new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.Name, "tester")], "test"))));
    }
}
