// <copyright file="SessionUnitTests.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>

using System.Text.Json;
using System.Text.Json.Nodes;
using OpenQA.Selenium.BiDi;

namespace OpenQA.Selenium.Tests.BiDi;

[Parallelizable(ParallelScope.All)]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
class SessionUnitTests
{
    private IBiDi _bidi;
    private FakeTransport _transport;

    [SetUp]
    public async Task SetUp()
    {
        _transport = new FakeTransport();
        _bidi = await Selenium.BiDi.BiDi.ConnectAsync(new Uri("ws://fake"), opts => opts.UseTransport(() => _transport));
    }

    [TearDown]
    public async Task TearDown()
    {
        await _bidi.DisposeAsync();
    }

    [Test]
    public void ShouldRespectCommandTimeout()
    {
        Assert.That(
            () => _bidi.StatusAsync(new() { Timeout = TimeSpan.FromMilliseconds(1) }),
            Throws.InstanceOf<TaskCanceledException>());
    }

    [Test]
    public void ShouldRespectCancellationToken()
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1));

        Assert.That(
            () => _bidi.StatusAsync(cancellationToken: cts.Token),
            Throws.InstanceOf<TaskCanceledException>());
    }

    [Test]
    public async Task ShouldHaveIdempotentDisposal()
    {
        await _bidi.DisposeAsync();
        await _bidi.DisposeAsync();
    }

    [Test]
    public async Task StatusCommandSendsCorrectMethod()
    {
        var status = await _bidi.StatusAsync()
            .WithResponse(_transport, """{"ready":true,"message":"running"}""");

        Assert.That(status.Message, Is.EqualTo("running"));

        Assert.That(_transport.SentMessages, Has.Count.EqualTo(1));
        using var doc = JsonDocument.Parse(_transport.SentMessages[0]);
        Assert.That(doc.RootElement.GetProperty("method").GetString(), Is.EqualTo("session.status"));
    }

    [Test]
    public async Task ErrorResponseThrowsBiDiException()
    {
        Assert.That(
            () => _bidi.StatusAsync()
                .WithErrorResponse(_transport, "invalid argument", "missing required field"),
            Throws.InstanceOf<BiDiException>()
                  .With.Message.Contains("invalid argument"));
    }

    [Test]
    public async Task CommandIdsAreSequential()
    {
        await _bidi.StatusAsync()
            .WithResponse(_transport, """{"ready":true,"message":"first"}""");
        await _bidi.StatusAsync()
            .WithResponse(_transport, """{"ready":true,"message":"second"}""");

        using var doc1 = JsonDocument.Parse(_transport.SentMessages[0]);
        using var doc2 = JsonDocument.Parse(_transport.SentMessages[1]);
        var id1 = doc1.RootElement.GetProperty("id").GetInt64();
        var id2 = doc2.RootElement.GetProperty("id").GetInt64();

        Assert.That(id2, Is.EqualTo(id1 + 1));
    }

    [Test]
    public async Task StatusResultExposesAdditionalData()
    {
        var status = await _bidi.StatusAsync()
            .WithResponse(_transport, """{"ready":true,"message":"running","foo":"value"}""");

        Assert.That(status.AdditionalData["foo"].GetString(), Is.EqualTo("value"));
    }

    [Test]
    public async Task StatusResultExposesAdditionalMessageData()
    {
        var status = await _bidi.StatusAsync()
            .WithRawResponse(_transport, """{"foo":"topLevel","id":1,"type":"success","result":{"ready":true,"message":"running"}}""");

        Assert.That(status.AdditionalMessageData["foo"].GetString(), Is.EqualTo("topLevel"));
    }

    [Test]
    public async Task CommandAdditionalDataIsSerializedIntoParams()
    {
        await _bidi.StatusAsync(new()
        {
            AdditionalData = new JsonObject { ["foo"] = "bar" }
        }).WithResponse(_transport, """{"ready":true,"message":"running"}""");

        using var doc = JsonDocument.Parse(_transport.SentMessages[0]);
        Assert.That(doc.RootElement.GetProperty("params").GetProperty("foo").GetString(), Is.EqualTo("bar"));
    }

    [Test]
    public async Task CommandAdditionalMessageDataIsSerializedAsTopLevelFields()
    {
        await _bidi.StatusAsync(new()
        {
            AdditionalMessageData = """{"baz": "qux"}"""
        }).WithResponse(_transport, """{"ready":true,"message":"running"}""");

        using var doc = JsonDocument.Parse(_transport.SentMessages[0]);
        Assert.That(doc.RootElement.GetProperty("baz").GetString(), Is.EqualTo("qux"));
        Assert.That(doc.RootElement.GetProperty("params").EnumerateObject().Any(p => p.Name == "baz"), Is.False,
            "AdditionalMessageData fields must be top-level, not inside params");
    }

    [Test]
    public void CommandAdditionalDataMustBeJsonObject()
    {
        Assert.That(
            () => new AdditionalData("""42"""),
            Throws.InstanceOf<ArgumentException>()
                  .With.Message.Contains("Additional data must be a JSON object."));
    }

    [Test]
    public async Task EventArgsExposesAdditionalData()
    {
        var stream = await _bidi.Script.RealmDestroyed.StreamAsync()
            .WithResponse(_transport, """{"subscription":"sub-1"}""");

        _transport.EnqueueEvent("script.realmDestroyed", """{"realm":"r-1","foo":"extra"}""");

        var received = await stream.ReadAllAsync().FirstAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5));

        Assert.That(received.AdditionalData["foo"].GetString(), Is.EqualTo("extra"));

        await stream.DisposeAsync().WithResponse(_transport);
    }

    [Test]
    public async Task EventArgsExposesAdditionalMessageData()
    {
        var stream = await _bidi.Script.RealmDestroyed.StreamAsync()
            .WithResponse(_transport, """{"subscription":"sub-1"}""");

        _transport.Enqueue("""{"type":"event","method":"script.realmDestroyed","params":{"realm":"r-1"},"bar":"topLevel"}""");

        var received = await stream.ReadAllAsync().FirstAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5));

        Assert.That(received.AdditionalMessageData["bar"].GetString(), Is.EqualTo("topLevel"));

        await stream.DisposeAsync().WithResponse(_transport);
    }

    [Test]
    public async Task StreamCanBeReadSequentially()
    {
        var stream = await _bidi.Script.RealmDestroyed.StreamAsync()
            .WithResponse(_transport, """{"subscription":"sub-1"}""");

        _transport.EnqueueEvent("script.realmDestroyed", """{"realm":"r-1"}""");
        _transport.EnqueueEvent("script.realmDestroyed", """{"realm":"r-2"}""");

        var first = await stream.ReadAllAsync().FirstAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5));
        var second = await stream.ReadAllAsync().FirstAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5));

        Assert.That(first.Realm.Id, Is.EqualTo("r-1"));
        Assert.That(second.Realm.Id, Is.EqualTo("r-2"));

        await stream.DisposeAsync().WithResponse(_transport);
    }
}
