using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using foodmarket.IntegrationTests.Support;
using foodmarket.Shared.Pos.V1;
using Xunit;

namespace foodmarket.IntegrationTests;

[Collection(ApiCollection.Name)]
public class PosSyncTests
{
    private readonly ApiFactory _factory;
    public PosSyncTests(ApiFactory factory) => _factory = factory;
    private static string RandomBarcode()
        => string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));

    [Fact]
    public async Task Sync_returns_products_prices_stocks()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"pos-sync-{Guid.NewGuid():N}");
        var refs = await api.LoadRefsAsync();
        var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 250m, RandomBarcode());

        // Подкормим остаток через Enter, чтобы было что синхронизировать.
        var enter = await api.Http.PostAsJsonAsync("/api/inventory/enters", new
        {
            date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
            notes = "seed", lines = new[] { new { productId = p1, quantity = 5m, unitCost = 100m } },
        });
        enter.EnsureSuccessStatusCode();
        var eid = (await enter.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
        (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();

        // POS sync без since → полная выгрузка.
        using var resp = await api.Http.GetAsync("/api/pos/v1/sync");
        resp.EnsureSuccessStatusCode();
        var sync = await resp.Content.ReadFromJsonAsync<PosSyncResponse>();
        sync!.Products.Should().Contain(p => p.Id.ToString() == p1);
        sync.Prices.Should().Contain(pr => pr.ProductId.ToString() == p1 && pr.Amount == 250m);
        sync.Stocks.Should().Contain(s => s.ProductId.ToString() == p1 && s.Quantity == 5m);
        sync.ServerTime.Should().BeBefore(DateTime.UtcNow.AddSeconds(5));
    }

    [Fact]
    public async Task Sync_with_since_returns_only_delta()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"pos-delta-{Guid.NewGuid():N}");
        var refs = await api.LoadRefsAsync();
        var pOld = await api.CreateProductAsync(refs, $"OLD-{Guid.NewGuid():N}", 100m, RandomBarcode());

        // Маркер времени между двумя продуктами.
        await Task.Delay(1100);
        var marker = DateTime.UtcNow;
        await Task.Delay(1100);

        var pNew = await api.CreateProductAsync(refs, $"NEW-{Guid.NewGuid():N}", 200m, RandomBarcode());

        using var resp = await api.Http.GetAsync($"/api/pos/v1/sync?since={Uri.EscapeDataString(marker.ToString("o"))}");
        resp.EnsureSuccessStatusCode();
        var sync = await resp.Content.ReadFromJsonAsync<PosSyncResponse>();
        sync!.Products.Should().Contain(p => p.Id.ToString() == pNew);
        sync.Products.Should().NotContain(p => p.Id.ToString() == pOld);
    }

    [Fact]
    public async Task Post_sales_batch_creates_retail_sales_and_decrements_stock()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"pos-sale-{Guid.NewGuid():N}");
        var refs = await api.LoadRefsAsync();
        var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());

        var enter = await api.Http.PostAsJsonAsync("/api/inventory/enters", new
        {
            date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
            notes = "seed", lines = new[] { new { productId = p1, quantity = 20m, unitCost = 50m } },
        });
        enter.EnsureSuccessStatusCode();
        var eid = (await enter.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
        (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
        (await api.StockOfAsync(refs.StoreId, p1)).Should().Be(20m);

        var batch = new PosSaleBatchDto
        {
            IdempotencyKey = Guid.NewGuid(),
            Sales = new[]
            {
                MakeSale(p1, qty: 2m, price: 100m),
                MakeSale(p1, qty: 3m, price: 100m),
                MakeSale(p1, qty: 1m, price: 100m),
                MakeSale(p1, qty: 4m, price: 100m),
                MakeSale(p1, qty: 1m, price: 100m),
            },
        };
        using var post = await api.Http.PostAsJsonAsync("/api/pos/v1/sales", batch);
        post.IsSuccessStatusCode.Should().BeTrue($"POST вернул {(int)post.StatusCode}: {await post.Content.ReadAsStringAsync()}");
        var resp = await post.Content.ReadFromJsonAsync<PosSaleBatchResponse>();
        resp!.Accepted.Should().HaveCount(5);
        resp.Failed.Should().BeEmpty();
        resp.ReplayedFromCache.Should().BeFalse();

        // 20 − (2+3+1+4+1) = 9 на складе.
        (await api.StockOfAsync(refs.StoreId, p1)).Should().Be(9m);
    }

    /// <summary>Ключевой тест задачи: повтор того же батча → no duplicates,
    /// тот же ответ, остаток не уменьшается дважды.</summary>
    [Fact]
    public async Task Replay_same_batch_returns_cached_response_no_duplicates()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"pos-idem-{Guid.NewGuid():N}");
        var refs = await api.LoadRefsAsync();
        var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
        var enter = await api.Http.PostAsJsonAsync("/api/inventory/enters", new
        {
            date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
            notes = "seed", lines = new[] { new { productId = p1, quantity = 10m, unitCost = 50m } },
        });
        enter.EnsureSuccessStatusCode();
        var eid = (await enter.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
        (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();

        var batch = new PosSaleBatchDto
        {
            IdempotencyKey = Guid.NewGuid(),
            Sales = new[]
            {
                MakeSale(p1, qty: 3m, price: 100m),
                MakeSale(p1, qty: 2m, price: 100m),
            },
        };

        using var first = await api.Http.PostAsJsonAsync("/api/pos/v1/sales", batch);
        first.EnsureSuccessStatusCode();
        var r1 = await first.Content.ReadFromJsonAsync<PosSaleBatchResponse>();
        r1!.Accepted.Should().HaveCount(2);
        r1.ReplayedFromCache.Should().BeFalse();
        (await api.StockOfAsync(refs.StoreId, p1)).Should().Be(5m);

        using var second = await api.Http.PostAsJsonAsync("/api/pos/v1/sales", batch);
        second.EnsureSuccessStatusCode();
        var r2 = await second.Content.ReadFromJsonAsync<PosSaleBatchResponse>();
        r2!.Accepted.Should().HaveCount(2);
        r2.ReplayedFromCache.Should().BeTrue();
        // ServerSaleId должны совпасть — это тот же чек, что и в первом ответе.
        r2.Accepted.Select(a => a.ServerSaleId).Should().BeEquivalentTo(r1.Accepted.Select(a => a.ServerSaleId));

        // Stock не списался дважды.
        (await api.StockOfAsync(refs.StoreId, p1)).Should().Be(5m);
    }

    /// <summary>Per-sale ClientSaleId идемпотентность: если ту же продажу
    /// прислали в РАЗНЫХ батчах (с разными idempotency-key'ями), сервер всё
    /// равно вернёт ссылку на ранее созданный чек, не создавая дубль.</summary>
    [Fact]
    public async Task ClientSaleId_idempotency_across_batches()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"pos-csid-{Guid.NewGuid():N}");
        var refs = await api.LoadRefsAsync();
        var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
        var enter = await api.Http.PostAsJsonAsync("/api/inventory/enters", new
        {
            date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
            notes = "seed", lines = new[] { new { productId = p1, quantity = 10m, unitCost = 50m } },
        });
        enter.EnsureSuccessStatusCode();
        var eid = (await enter.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
        (await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();

        var sharedSale = MakeSale(p1, qty: 4m, price: 100m);
        var batch1 = new PosSaleBatchDto { IdempotencyKey = Guid.NewGuid(), Sales = new[] { sharedSale } };
        var batch2 = new PosSaleBatchDto { IdempotencyKey = Guid.NewGuid(), Sales = new[] { sharedSale } };

        var r1 = await (await api.Http.PostAsJsonAsync("/api/pos/v1/sales", batch1)).Content.ReadFromJsonAsync<PosSaleBatchResponse>();
        var r2 = await (await api.Http.PostAsJsonAsync("/api/pos/v1/sales", batch2)).Content.ReadFromJsonAsync<PosSaleBatchResponse>();
        r1!.Accepted.Should().HaveCount(1);
        r2!.Accepted.Should().HaveCount(1);
        r2.Accepted[0].ServerSaleId.Should().Be(r1.Accepted[0].ServerSaleId);
        (await api.StockOfAsync(refs.StoreId, p1)).Should().Be(6m, "товар списался 1 раз");
    }

    [Fact]
    public async Task Insufficient_stock_goes_to_failed_not_accepted()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"pos-short-{Guid.NewGuid():N}");
        var refs = await api.LoadRefsAsync();
        var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
        // Не подкармливаем — остаток 0.

        var batch = new PosSaleBatchDto
        {
            IdempotencyKey = Guid.NewGuid(),
            Sales = new[] { MakeSale(p1, qty: 3m, price: 100m) },
        };
        using var post = await api.Http.PostAsJsonAsync("/api/pos/v1/sales", batch);
        post.EnsureSuccessStatusCode();
        var resp = await post.Content.ReadFromJsonAsync<PosSaleBatchResponse>();
        resp!.Accepted.Should().BeEmpty();
        resp.Failed.Should().HaveCount(1);
        resp.Failed[0].Error.Should().Contain("Недостаточный остаток");
    }

    [Fact]
    public async Task Tenant_isolation_pos_sync()
    {
        var a = new ApiActor(_factory.CreateClient());
        var b = new ApiActor(_factory.CreateClient());
        await a.SignupAndLoginAsync($"pos-iso-a-{Guid.NewGuid():N}");
        await b.SignupAndLoginAsync($"pos-iso-b-{Guid.NewGuid():N}");
        var refsA = await a.LoadRefsAsync();
        var pA = await a.CreateProductAsync(refsA, $"PA-{Guid.NewGuid():N}", 100m, RandomBarcode());

        var syncB = await (await b.Http.GetAsync("/api/pos/v1/sync")).Content.ReadFromJsonAsync<PosSyncResponse>();
        syncB!.Products.Should().NotContain(p => p.Id.ToString() == pA);
    }

    private static PosSaleDto MakeSale(string productId, decimal qty, decimal price) => new()
    {
        ClientSaleId = Guid.NewGuid(),
        OccurredAt = DateTime.UtcNow,
        Payment = 0, PaidCash = qty * price, PaidCard = 0m,
        Lines = new[]
        {
            new PosSaleLineDto
            {
                ProductId = Guid.Parse(productId), Quantity = qty,
                UnitPrice = price, Discount = 0m, VatPercent = 12m,
            },
        },
    };
}
