using Meezi.API.Models.Orders;
using Meezi.API.Services;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Xunit;

namespace Meezi.API.Tests;

public class ShiftServiceTests
{
    private static (
        AppDbContext Db,
        ShiftService Shifts,
        OrderService Orders,
        string CafeId,
        string BranchId,
        string UserId,
        string MenuItemId)
        CreateFixture()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        var db = new AppDbContext(options);

        var cafeId = "cafe-shift";
        var branchId = "branch-main";
        var userId = "emp-cashier";
        var menuItemId = "item-1";
        var categoryId = "cat-1";

        db.Cafes.Add(new Cafe { Id = cafeId, Name = "Shift Cafe", Slug = "shift-cafe", PlanTier = PlanTier.Pro });
        db.Branches.Add(new Branch
        {
            Id = branchId,
            CafeId = cafeId,
            Name = "Main",
            IsActive = true,
            UpdatedAt = DateTime.UtcNow
        });
        db.Employees.Add(new Employee
        {
            Id = userId,
            CafeId = cafeId,
            BranchId = branchId,
            Name = "Cashier",
            Phone = "09121111111",
            Role = EmployeeRole.Cashier
        });
        db.MenuCategories.Add(new MenuCategory
        {
            Id = categoryId,
            CafeId = cafeId,
            Name = "Menu",
            NameEn = "Menu",
            SortOrder = 0
        });
        db.MenuItems.Add(new MenuItem
        {
            Id = menuItemId,
            CafeId = cafeId,
            CategoryId = categoryId,
            Name = "Tea",
            NameEn = "Tea",
            Price = 100_000m,
            IsAvailable = true
        });
        db.SaveChanges();

        var shifts = new ShiftService(db);
        var orders = new OrderService(db, new NoOpKdsNotifier(), new NoOpSnappfood(), new NoOpDeliverySync(), shifts, TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService());
        return (db, shifts, orders, cafeId, branchId, userId, menuItemId);
    }

    [Fact]
    public async Task OpenShift_Succeeds()
    {
        var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture();

        var result = await shifts.OpenShiftAsync(cafeId, branchId, 500_000m, userId);

        Assert.True(result.Success);
        Assert.Equal(ShiftStatus.Open, result.Data!.Status);
        Assert.Equal(500_000m, result.Data.OpeningCash);
    }

    [Fact]
    public async Task OpenShift_WhenAlreadyOpen_ReturnsShiftAlreadyOpen()
    {
        var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture();

        await shifts.OpenShiftAsync(cafeId, branchId, 100m, userId);
        var second = await shifts.OpenShiftAsync(cafeId, branchId, 200m, userId);

        Assert.False(second.Success);
        Assert.Equal("SHIFT_ALREADY_OPEN", second.ErrorCode);
    }

    [Fact]
    public async Task CloseShift_CalculatesDiscrepancy_FromCashPaymentsAndWithdrawals()
    {
        var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture();

        var opened = await shifts.OpenShiftAsync(cafeId, branchId, 1_000_000m, userId);
        var shiftId = opened.Data!.Id;

        await shifts.RecordTransactionAsync(
            cafeId, shiftId, CashTransactionType.OrderPayment, PaymentMethod.Cash, 300_000m, userId);
        await shifts.RecordTransactionAsync(
            cafeId, shiftId, CashTransactionType.OrderPayment, PaymentMethod.Card, 200_000m, userId);
        await shifts.RecordTransactionAsync(
            cafeId, shiftId, CashTransactionType.Withdrawal, PaymentMethod.Cash, 50_000m, userId);

        var closed = await shifts.CloseShiftAsync(cafeId, shiftId, 1_300_000m, userId);

        Assert.True(closed.Success);
        Assert.Equal(1_250_000m, closed.Data!.ExpectedCash);
        Assert.Equal(50_000m, closed.Data.Discrepancy);
        Assert.Equal(ShiftStatus.Closed, closed.Data.Status);
    }

    [Fact]
    public async Task RecordPayments_WithOpenShift_RecordsCashTransactions()
    {
        var (db, shifts, orders, cafeId, branchId, userId, menuItemId) = CreateFixture();
        await shifts.OpenShiftAsync(cafeId, branchId, 0m, userId);

        var tenant = new TenantContext { CafeId = cafeId, UserId = userId, Role = EmployeeRole.Cashier };
        var created = await orders.CreateOrderAsync(
            cafeId,
            tenant,
            new CreateOrderRequest(
                OrderType.DineIn,
                branchId,
                null,
                null,
                "Guest",
                null,
                null,
                null,
                [new CreateOrderItemRequest(menuItemId, 1, null)]));
        Assert.True(created.Success);

        var pay = await orders.RecordPaymentsAsync(
            cafeId,
            created.Data!.Id,
            new RecordPaymentsRequest(
                [new CreatePaymentRequest(PaymentMethod.Cash, 100_000m, null),
                 new CreatePaymentRequest(PaymentMethod.Card, 50_000m, null)]),
            userId);

        Assert.True(pay.Success);

        var shift = await db.RegisterShifts.Include(s => s.Transactions)
            .FirstAsync(s => s.BranchId == branchId && s.Status == ShiftStatus.Open);
        Assert.Equal(2, shift.Transactions.Count);
        Assert.Contains(shift.Transactions, t => t.Method == PaymentMethod.Cash && t.Amount == 100_000m);
    }

    [Fact]
    public async Task RecordPayments_WithoutOpenShift_ReturnsNoOpenShift()
    {
        var (_, _, orders, cafeId, branchId, userId, menuItemId) = CreateFixture();

        var tenant = new TenantContext { CafeId = cafeId, UserId = userId };
        var created = await orders.CreateOrderAsync(
            cafeId,
            tenant,
            new CreateOrderRequest(
                OrderType.DineIn,
                branchId,
                null,
                null,
                "Guest",
                null,
                null,
                null,
                [new CreateOrderItemRequest(menuItemId, 1, null)]));
        Assert.True(created.Success);

        var pay = await orders.RecordPaymentsAsync(
            cafeId,
            created.Data!.Id,
            new RecordPaymentsRequest([new CreatePaymentRequest(PaymentMethod.Cash, 100_000m, null)]),
            userId);

        Assert.False(pay.Success);
        Assert.Equal("NO_OPEN_SHIFT", pay.ErrorCode);
    }

    private sealed class NoOpKdsNotifier : IKdsNotifier
    {
        public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
        public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
        public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
    }

    private sealed class NoOpSnappfood : ISnappfoodClient
    {
        public Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
        public Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
        public Task NotifyOrderStatusAsync(string snappfoodOrderId, string status, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
    }
}
