// Copyright © WireMock.Net

using System.Net;
using System.Net.Http;

using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;

namespace WireMock.Net.Tests;

public class StatefulBehaviorTests
{
    [Fact]
    public async Task Scenarios_Should_skip_non_relevant_states()
    {
        // given
        string path = $"/foo_{Guid.NewGuid()}";
        var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("s")
            .WhenStateIs("Test state")
            .RespondWith(Response.Create());

        // when
        var response = await new HttpClient().GetAsync("http://localhost:" + server.Ports[0] + path, TestContext.Current.CancellationToken);

        // then
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);

        server.Stop();
    }

    [Fact]
    public async Task Scenarios_Should_process_request_if_equals_state_and_single_state_defined()
    {
        // Arrange
        var cancellationToken = TestContext.Current.CancellationToken;
        var path = $"/foo_{Guid.NewGuid()}";
        using var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("s")
            .WillSetStateTo("Test state")
            .RespondWith(Response.Create().WithBody("No state msg"));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("s")
            .WhenStateIs("Test state")
            .RespondWith(Response.Create().WithBody("Test state msg"));

        // Act
        var responseNoState = await new HttpClient().GetStringAsync(server.Url + path, cancellationToken);
        var responseWithState = await new HttpClient().GetStringAsync(server.Url + path, cancellationToken);

        // Assert
        responseNoState.Should().Be("No state msg");
        responseWithState.Should().Be("Test state msg");
    }

    [Theory]
    [InlineData(null, "step 1", "step 2")]
    [InlineData("step 2", "step 2", "step 3")]
    public async Task Scenarios_Should_ContinueOnCorrectState_WhenStateIsUpdated(string? state, string expected1, string expected2)
    {
        // Arrange
        var cancellationToken = TestContext.Current.CancellationToken;
        var path = $"/foo_{Guid.NewGuid()}";
        var scenario = "s";
        using var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario(scenario)
            .WillSetStateTo("step 2")
            .RespondWith(Response.Create().WithBody("step 1"));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario(scenario)
            .WhenStateIs("step 2")
            .WillSetStateTo("step 3")
            .RespondWith(Response.Create().WithBody("step 2"));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario(scenario)
            .WhenStateIs("step 3")
            .RespondWith(Response.Create().WithBody("step 3"));

        // Act
        var client = server.CreateClient();
        var response1 = await client.GetStringAsync(server.Url + path, cancellationToken);
        var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
        var response3 = await client.GetStringAsync(server.Url + path, cancellationToken);

        server.SetScenarioState(scenario, state);
        var responseA = await client.GetStringAsync(server.Url + path, cancellationToken);
        var responseB = await client.GetStringAsync(server.Url + path, cancellationToken);

        // Assert
        response1.Should().Be("step 1");
        response2.Should().Be("step 2");
        response3.Should().Be("step 3");
        responseA.Should().Be(expected1);
        responseB.Should().Be(expected2);
    }

    [Fact]
    public async Task Scenarios_With_Same_Path_Should_Use_Times_When_Moving_To_Next_State()
    {
        // given
        var cancellationToken = TestContext.Current.CancellationToken;
        const int times = 2;
        string path = $"/foo_{Guid.NewGuid()}";
        string body1 = "Scenario S1, No State, Setting State T2";
        string body2 = "Scenario S1, State T2, End";
        var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario(1)
            .WillSetStateTo(2, times)
            .RespondWith(Response.Create().WithBody(body1));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario(1)
            .WhenStateIs(2)
            .RespondWith(Response.Create().WithBody(body2));

        // when
        var client = new HttpClient();
        var responseScenario1 = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
        var responseScenario2 = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
        var responseWithState = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);

        // then
        responseScenario1.Should().Be(body1);
        responseScenario2.Should().Be(body1);
        responseWithState.Should().Be(body2);

        server.Stop();
    }

    [Fact]
    public async Task Scenarios_With_Different_Paths_Should_Use_Times_When_Moving_To_Next_State()
    {
        // given
        var cancellationToken = TestContext.Current.CancellationToken;
        const int times = 2;
        string path1 = $"/a_{Guid.NewGuid()}";
        string path2 = $"/b_{Guid.NewGuid()}";
        string path3 = $"/c_{Guid.NewGuid()}";
        string body1 = "Scenario S1, No State, Setting State T2";
        string body2 = "Scenario S1, State T2, Setting State T3";
        string body3 = "Scenario S1, State T3, End";

        var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath(path1).UsingGet())
            .InScenario("S1")
            .WillSetStateTo("T2", times)
            .RespondWith(Response.Create().WithBody(body1));

        server
            .Given(Request.Create().WithPath(path2).UsingGet())
            .InScenario("S1")
            .WhenStateIs("T2")
            .WillSetStateTo("T3", times)
            .RespondWith(Response.Create().WithBody(body2));

        server
            .Given(Request.Create().WithPath(path3).UsingGet())
            .InScenario("S1")
            .WhenStateIs("T3")
            .RespondWith(Response.Create().WithBody(body3));

        // when
        var client = new HttpClient();
        var t1a = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path1, cancellationToken);
        var t1b = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path1, cancellationToken);
        var t2a = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path2, cancellationToken);
        var t2b = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path2, cancellationToken);
        var t3 = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path3, cancellationToken);

        // then
        t1a.Should().Be(body1);
        t1b.Should().Be(body1);
        t2a.Should().Be(body2);
        t2b.Should().Be(body2);
        t3.Should().Be(body3);

        server.Stop();
    }

    [Fact]
    public async Task Scenarios_Should_Respect_Int_Valued_Scenarios_and_States()
    {
        // given
        var cancellationToken = TestContext.Current.CancellationToken;
        string path = $"/foo_{Guid.NewGuid()}";
        var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario(1)
            .WillSetStateTo(2)
            .RespondWith(Response.Create().WithBody("Scenario 1, Setting State 2"));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario(1)
            .WhenStateIs(2)
            .RespondWith(Response.Create().WithBody("Scenario 1, State 2"));

        // when
        var responseIntScenario = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
        var responseWithIntState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);

        // then
        responseIntScenario.Should().Be("Scenario 1, Setting State 2");
        responseWithIntState.Should().Be("Scenario 1, State 2");

        server.Stop();
    }

    [Fact]
    public async Task Scenarios_Should_Respect_Mixed_String_Scenario_and_Int_State()
    {
        // given
        var cancellationToken = TestContext.Current.CancellationToken;
        string path = $"/foo_{Guid.NewGuid()}";
        var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("state string")
            .WillSetStateTo(1)
            .RespondWith(Response.Create().WithBody("string state, Setting State 2"));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("state string")
            .WhenStateIs(1)
            .RespondWith(Response.Create().WithBody("string state, State 2"));

        // when
        var responseIntScenario = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
        var responseWithIntState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);

        // then
        responseIntScenario.Should().Be("string state, Setting State 2");
        responseWithIntState.Should().Be("string state, State 2");

        server.Stop();
    }

    [Fact]
    public async Task Scenarios_Should_Respect_Mixed_Int_Scenario_and_String_Scenario_and_String_State()
    {
        // given
        var cancellationToken = TestContext.Current.CancellationToken;
        string path = $"/foo_{Guid.NewGuid()}";
        var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario(1)
            .WillSetStateTo("Next State")
            .RespondWith(Response.Create().WithBody("int state, Setting State 2"));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("1")
            .WhenStateIs("Next State")
            .RespondWith(Response.Create().WithBody("string state, State 2"));

        // when
        var responseIntScenario = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
        var responseWithIntState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);

        // then
        responseIntScenario.Should().Be("int state, Setting State 2");
        responseWithIntState.Should().Be("string state, State 2");

        server.Stop();
    }

    [Fact]
    public async Task Scenarios_TodoList_Example()
    {
        // Arrange
        var cancelationToken = TestContext.Current.CancellationToken;
        var server = WireMockServer.Start();
        var client = server.CreateClient();

        server
            .Given(Request.Create().WithPath("/todo/items").UsingGet())
            .InScenario("To do list")
            .WillSetStateTo("TodoList State Started")
            .RespondWith(Response.Create().WithBody("Buy milk"));

        server
            .Given(Request.Create().WithPath("/todo/items").UsingPost())
            .InScenario("To do list")
            .WhenStateIs("TodoList State Started")
            .WillSetStateTo("Cancel newspaper item added")
            .RespondWith(Response.Create().WithStatusCode(201));

        server
            .Given(Request.Create().WithPath("/todo/items").UsingGet())
            .InScenario("To do list")
            .WhenStateIs("Cancel newspaper item added")
            .RespondWith(Response.Create().WithBody("Buy milk;Cancel newspaper subscription"));

        server.Scenarios.Any().Should().BeFalse();

        // Act and Assert
        var getResponse1 = await client.GetStringAsync("/todo/items", cancelationToken);
        getResponse1.Should().Be("Buy milk");

        server.Scenarios.First(s => s.Name == "To do list").Name.Should().Be("To do list");
        server.Scenarios.First(s => s.Name == "To do list").NextState.Should().Be("TodoList State Started");
        server.Scenarios.First(s => s.Name == "To do list").Started.Should().BeTrue();
        server.Scenarios.First(s => s.Name == "To do list").Finished.Should().BeFalse();

        var postResponse = await client.PostAsync("/todo/items", new StringContent("Cancel newspaper subscription"), cancelationToken);
        postResponse.StatusCode.Should().Be(HttpStatusCode.Created);

        server.Scenarios.First(s => s.Name == "To do list").Name.Should().Be("To do list");
        server.Scenarios.First(s => s.Name == "To do list").NextState.Should().Be("Cancel newspaper item added");
        server.Scenarios.First(s => s.Name == "To do list").Started.Should().BeTrue();
        server.Scenarios.First(s => s.Name == "To do list").Finished.Should().BeFalse();

        string getResponse2 = await client.GetStringAsync("/todo/items", cancelationToken);
        getResponse2.Should().Be("Buy milk;Cancel newspaper subscription");

        server.Scenarios.First(s => s.Name == "To do list").Name.Should().Be("To do list");
        server.Scenarios.First(s => s.Name == "To do list").NextState.Should().BeNull();
        server.Scenarios.First(s => s.Name == "To do list").Started.Should().BeTrue();
        server.Scenarios.First(s => s.Name == "To do list").Finished.Should().BeTrue();

        server.Stop();
    }

    [Fact]
    public async Task Scenarios_TodoList_WithSetState()
    {
        // Arrange
        var cancelationToken = TestContext.Current.CancellationToken;
        var scenario = "To do list";
        using var server = WireMockServer.Start();
        var client = server.CreateClient();

        server
            .Given(Request.Create().WithPath("/todo/items").UsingGet())
            .InScenario(scenario)
            .WhenStateIs("Buy milk")
            .RespondWith(Response.Create().WithBody("Buy milk"));

        server
            .Given(Request.Create().WithPath("/todo/items").UsingGet())
            .InScenario(scenario)
            .WhenStateIs("Cancel newspaper")
            .RespondWith(Response.Create().WithBody("Buy milk;Cancel newspaper subscription"));

        // Act and Assert
        server.SetScenarioState(scenario, "Buy milk");
        server.Scenarios.First(s => s.Name == scenario).Should().BeEquivalentTo(new { Name = scenario, NextState = "Buy milk" });

        var getResponse1 = await client.GetStringAsync("/todo/items", cancelationToken);
        getResponse1.Should().Be("Buy milk");

        server.SetScenarioState(scenario, "Cancel newspaper");
        server.Scenarios.First(s => s.Name == scenario).Name.Should().Be(scenario);
        server.Scenarios.First(s => s.Name == scenario).Should().BeEquivalentTo(new { Name = scenario, NextState = "Cancel newspaper" });

        var getResponse2 = await client.GetStringAsync("/todo/items", cancelationToken);
        getResponse2.Should().Be("Buy milk;Cancel newspaper subscription");
    }

    [Fact]
    public void Scenarios_TodoList_WithSetStateToNull_ShouldThrowException()
    {
        // Arrange
        var scenario = "To do list";
        using var server = WireMockServer.Start();
        var client = server.CreateClient();

        server
            .Given(Request.Create().WithPath("/todo/items").UsingGet())
            .InScenario(scenario)
            .WhenStateIs("Buy milk")
            .RespondWith(Response.Create().WithBody("Buy milk"));

        server
            .Given(Request.Create().WithPath("/todo/items").UsingGet())
            .InScenario(scenario)
            .WhenStateIs("Cancel newspaper")
            .RespondWith(Response.Create().WithBody("Buy milk;Cancel newspaper subscription"));

        // Act
        server.SetScenarioState(scenario, null);
        var action = async () => await client.GetStringAsync("/todo/items");

        // Assert
        action.Should().ThrowAsync<HttpRequestException>();
    }

    [Fact]
    public async Task Scenarios_FirstRequestWithDelay_StateTransitions_BeforeDelayCompletes()
    {
        // Arrange
        var cancellationToken = TestContext.Current.CancellationToken;
        var path = $"/foo_{Guid.NewGuid()}";
        using var server = WireMockServer.Start();

        // Mapping 1: start state, has a 500 ms delay
        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("1260")
            .WillSetStateTo("State1")
            .RespondWith(Response.Create()
                .WithBody("delayed response")
                .WithDelay(TimeSpan.FromMilliseconds(500)));

        // Mapping 2: only matches after state has transitioned to "State1"
        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("1260")
            .WhenStateIs("State1")
            .RespondWith(Response.Create().WithBody("immediate response"));

        var client = new HttpClient();

        // Act: fire request 1 but don't await it yet — it will sit in a 500 ms delay
        var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);

        // Give the server a moment to match & transition state before the delay completes
        await Task.Delay(100, cancellationToken);

        // Request 2 is sent while request 1 is still being delayed.
        // After the fix the state has already transitioned, so request 2 matches Mapping 2.
        var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);

        var response1 = await request1Task;

        // Assert
        response1.Should().Be("delayed response");
        response2.Should().Be("immediate response");
    }

    [Fact]
    public async Task Scenarios_WithGlobalRequestProcessingDelay_StateTransitions_BeforeDelayCompletes()
    {
        // Arrange: use the global RequestProcessingDelay instead of a per-mapping delay
        var cancellationToken = TestContext.Current.CancellationToken;
        var path = $"/foo_{Guid.NewGuid()}";
        using var server = WireMockServer.Start();
        server.AddGlobalProcessingDelay(TimeSpan.FromMilliseconds(500));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("s")
            .WillSetStateTo("State1")
            .RespondWith(Response.Create().WithBody("delayed response"));

        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("s")
            .WhenStateIs("State1")
            .RespondWith(Response.Create().WithBody("immediate response"));

        var client = new HttpClient();

        // Act
        var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
        await Task.Delay(100, cancellationToken);
        var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
        var response1 = await request1Task;

        // Assert
        response1.Should().Be("delayed response");
        response2.Should().Be("immediate response");
    }

    [Fact]
    public async Task Scenarios_WithDelay_And_TimesInSameState_Should_Transition_After_Required_Hits()
    {
        // Arrange
        var cancellationToken = TestContext.Current.CancellationToken;
        var path = $"/foo_{Guid.NewGuid()}";
        using var server = WireMockServer.Start();

        // Mapping 1: requires 2 hits before transitioning; has a short delay
        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("s")
            .WillSetStateTo("State1", 2)
            .RespondWith(Response.Create()
                .WithBody("first")
                .WithDelay(TimeSpan.FromMilliseconds(50)));

        // Mapping 2: matches after state is "State1"
        server
            .Given(Request.Create().WithPath(path).UsingGet())
            .InScenario("s")
            .WhenStateIs("State1")
            .RespondWith(Response.Create().WithBody("second"));

        var client = new HttpClient();

        // Act
        var response1 = await client.GetStringAsync(server.Url + path, cancellationToken);
        var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
        var response3 = await client.GetStringAsync(server.Url + path, cancellationToken);

        // Assert: state only transitions after 2 hits, so request 3 is the first to match Mapping 2
        response1.Should().Be("first");
        response2.Should().Be("first");
        response3.Should().Be("second");
    }

    [Fact]
    public async Task Scenarios_Should_process_request_if_equals_state_and_multiple_state_defined()
    {
        // Assign
        var cancelationToken = TestContext.Current.CancellationToken;
        var server = WireMockServer.Start();

        server
            .Given(Request.Create().WithPath("/state1").UsingGet())
            .InScenario("s1")
            .WillSetStateTo("Test state 1")
            .RespondWith(Response.Create().WithBody("No state msg 1"));

        server
            .Given(Request.Create().WithPath("/foo1X").UsingGet())
            .InScenario("s1")
            .WhenStateIs("Test state 1")
            .RespondWith(Response.Create().WithBody("Test state msg 1"));

        server
            .Given(Request.Create().WithPath("/state2").UsingGet())
            .InScenario("s2")
            .WillSetStateTo("Test state 2")
            .RespondWith(Response.Create().WithBody("No state msg 2"));

        server
            .Given(Request.Create().WithPath("/foo2X").UsingGet())
            .InScenario("s2")
            .WhenStateIs("Test state 2")
            .RespondWith(Response.Create().WithBody("Test state msg 2"));

        // Act and Assert
        string url = "http://localhost:" + server.Ports[0];
        var responseNoState1 = await new HttpClient().GetStringAsync(url + "/state1", cancelationToken);
        responseNoState1.Should().Be("No state msg 1");

        var responseNoState2 = await new HttpClient().GetStringAsync(url + "/state2", cancelationToken);
        responseNoState2.Should().Be("No state msg 2");

        var responseWithState1 = await new HttpClient().GetStringAsync(url + "/foo1X", cancelationToken);
        responseWithState1.Should().Be("Test state msg 1");

        var responseWithState2 = await new HttpClient().GetStringAsync(url + "/foo2X", cancelationToken);
        responseWithState2.Should().Be("Test state msg 2");

        server.Stop();
    }
}

