using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Infrastructure.Repositories;
using PowderCoating.Web.Controllers;

namespace PowderCoating.UnitTests;

public class DepositsControllerTests
{
    // ── Record — input validation (no user resolution needed) ─────────────

    [Fact]
    public async Task Record_WhenAmountIsZero_ReturnsJsonError()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: CreateUser(1));

        var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
            amount: 0m, paymentMethod: "Cash",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        using var doc = ParseJson(result);
        Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
        Assert.Contains("greater than zero", doc.RootElement.GetProperty("message").GetString());
    }

    [Fact]
    public async Task Record_WhenAmountIsNegative_ReturnsJsonError()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: CreateUser(1));

        var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
            amount: -50m, paymentMethod: "Cash",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        using var doc = ParseJson(result);
        Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
    }

    [Fact]
    public async Task Record_WhenNeitherJobNorQuoteProvided_ReturnsJsonError()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: CreateUser(1));

        var result = await controller.Record(jobId: null, quoteId: null, customerId: 1,
            amount: 100m, paymentMethod: "Cash",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        using var doc = ParseJson(result);
        Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
        Assert.Contains("Job or Quote must be specified", doc.RootElement.GetProperty("message").GetString());
    }

    [Fact]
    public async Task Record_WhenInvalidPaymentMethod_ReturnsJsonError()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: CreateUser(1));

        var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
            amount: 100m, paymentMethod: "BitcoinPizza",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        using var doc = ParseJson(result);
        Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
        Assert.Contains("Invalid payment method", doc.RootElement.GetProperty("message").GetString());
    }

    [Fact]
    public async Task Record_WhenUserNotFound_ReturnsUnauthorized()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: null);

        var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
            amount: 100m, paymentMethod: "Cash",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        Assert.IsType<UnauthorizedResult>(result);
    }

    // ── Record — success paths ────────────────────────────────────────────

    [Fact]
    public async Task Record_WithJobId_CreatesDepositAndReturnsSuccessJson()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: CreateUser(companyId: 1));
        var receivedDate = new DateTime(2026, 4, 15);

        var result = await controller.Record(jobId: 42, quoteId: null, customerId: 7,
            amount: 250m, paymentMethod: "Check",
            receivedDate: receivedDate, reference: "REF-001", notes: "Half up front");

        using var doc = ParseJson(result);
        Assert.True(doc.RootElement.GetProperty("success").GetBoolean());
        Assert.Equal(250m, doc.RootElement.GetProperty("amount").GetDecimal());
        Assert.Equal("Check", doc.RootElement.GetProperty("paymentMethod").GetString());
        Assert.Equal("04/15/2026", doc.RootElement.GetProperty("receivedDate").GetString());
        Assert.Equal("REF-001", doc.RootElement.GetProperty("reference").GetString());
        Assert.Equal("Half up front", doc.RootElement.GetProperty("notes").GetString());

        var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync();
        Assert.Equal(42, deposit.JobId);
        Assert.Null(deposit.QuoteId);
        Assert.Equal(7, deposit.CustomerId);
        Assert.Equal(250m, deposit.Amount);
        Assert.Equal(PaymentMethod.Check, deposit.PaymentMethod);
    }

    [Fact]
    public async Task Record_WithQuoteId_CreatesDepositLinkedToQuote()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: CreateUser(companyId: 1));

        var result = await controller.Record(jobId: null, quoteId: 99, customerId: 5,
            amount: 500m, paymentMethod: "CreditDebitCard",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        using var doc = ParseJson(result);
        Assert.True(doc.RootElement.GetProperty("success").GetBoolean());
        Assert.Equal("Credit / Debit Card", doc.RootElement.GetProperty("paymentMethod").GetString());

        var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync();
        Assert.Equal(99, deposit.QuoteId);
        Assert.Null(deposit.JobId);
    }

    [Fact]
    public async Task Record_GeneratesReceiptNumberInExpectedFormat()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: CreateUser(companyId: 1));

        var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
            amount: 100m, paymentMethod: "Cash",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        using var doc = ParseJson(result);
        var receiptNumber = doc.RootElement.GetProperty("receiptNumber").GetString()!;

        // Format: DEP-YYMM-#### (e.g. DEP-2604-0001)
        Assert.Matches(@"^DEP-\d{4}-\d{4}$", receiptNumber);
        Assert.EndsWith("-0001", receiptNumber);
    }

    [Fact]
    public async Task Record_SecondDepositInSameMonth_IncrementsSequenceNumber()
    {
        await using var context = CreateContext();
        var user = CreateUser(companyId: 1);
        var controller = CreateController(context, currentUser: user);

        // First deposit
        await controller.Record(jobId: 1, quoteId: null, customerId: 1,
            amount: 100m, paymentMethod: "Cash",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        // Second deposit
        var result = await controller.Record(jobId: 2, quoteId: null, customerId: 1,
            amount: 200m, paymentMethod: "Cash",
            receivedDate: DateTime.UtcNow, reference: null, notes: null);

        using var doc = ParseJson(result);
        var receiptNumber = doc.RootElement.GetProperty("receiptNumber").GetString()!;
        Assert.EndsWith("-0002", receiptNumber);
    }

    [Fact]
    public async Task Record_PaymentMethodDisplayStrings_AreHumanReadable()
    {
        await using var context = CreateContext();
        var user = CreateUser(companyId: 1);

        var cases = new[]
        {
            ("Cash", "Cash"),
            ("Check", "Check"),
            ("CreditDebitCard", "Credit / Debit Card"),
            ("BankTransferACH", "Bank Transfer / ACH"),
            ("DigitalPayment", "Digital Payment"),
        };

        foreach (var (method, expected) in cases)
        {
            var controller = CreateController(context, currentUser: user);
            var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
                amount: 10m, paymentMethod: method,
                receivedDate: DateTime.UtcNow, reference: null, notes: null);

            using var doc = ParseJson(result);
            if (doc.RootElement.GetProperty("success").GetBoolean())
                Assert.Equal(expected, doc.RootElement.GetProperty("paymentMethod").GetString());
        }
    }

    // ── Delete ────────────────────────────────────────────────────────────

    [Fact]
    public async Task Delete_WhenDepositNotFound_ReturnsJsonError()
    {
        await using var context = CreateContext();
        var controller = CreateController(context, currentUser: CreateUser(1));

        var result = await controller.Delete(id: 999, returnUrl: null);

        using var doc = ParseJson(result);
        Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
        Assert.Contains("not found", doc.RootElement.GetProperty("message").GetString());
    }

    [Fact]
    public async Task Delete_WhenDepositAlreadyAppliedToInvoice_ReturnsJsonError()
    {
        await using var context = CreateContext();
        context.Deposits.Add(new Deposit
        {
            Id = 1,
            CompanyId = 1,
            ReceiptNumber = "DEP-2604-0001",
            CustomerId = 1,
            JobId = 1,
            Amount = 100m,
            PaymentMethod = PaymentMethod.Cash,
            ReceivedDate = DateTime.UtcNow,
            AppliedToInvoiceId = 55  // already applied
        });
        await context.SaveChangesAsync();
        var controller = CreateController(context, currentUser: CreateUser(1));

        var result = await controller.Delete(id: 1, returnUrl: null);

        using var doc = ParseJson(result);
        Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
        Assert.Contains("already been applied", doc.RootElement.GetProperty("message").GetString());
    }

    [Fact]
    public async Task Delete_WhenUnappliedDeposit_SoftDeletesAndReturnsSuccess()
    {
        await using var context = CreateContext();
        context.Deposits.Add(new Deposit
        {
            Id = 2,
            CompanyId = 1,
            ReceiptNumber = "DEP-2604-0001",
            CustomerId = 1,
            JobId = 1,
            Amount = 75m,
            PaymentMethod = PaymentMethod.Cash,
            ReceivedDate = DateTime.UtcNow,
            AppliedToInvoiceId = null  // not yet applied
        });
        await context.SaveChangesAsync();
        var controller = CreateController(context, currentUser: CreateUser(1));

        var result = await controller.Delete(id: 2, returnUrl: null);

        using var doc = ParseJson(result);
        Assert.True(doc.RootElement.GetProperty("success").GetBoolean());

        var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync(d => d.Id == 2);
        Assert.True(deposit.IsDeleted);
    }

    // ── Helpers ───────────────────────────────────────────────────────────

    private static DepositsController CreateController(
        ApplicationDbContext context,
        ApplicationUser? currentUser)
    {
        var uow = new UnitOfWork(context);
        var userManager = CreateUserManagerMock();
        userManager
            .Setup(m => m.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
            .ReturnsAsync(currentUser);

        var controller = new DepositsController(
            uow,
            userManager.Object,
            Mock.Of<ILogger<DepositsController>>(),
            Mock.Of<ICompanyLogoService>(),
            Mock.Of<IAccountBalanceService>());

        controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext()
        };

        return controller;
    }

    private static ApplicationUser CreateUser(int companyId) => new()
    {
        Id = "test-user",
        CompanyId = companyId,
        UserName = "test@example.com",
        Email = "test@example.com",
        FirstName = "Test",
        LastName = "User"
    };

    private static Mock<UserManager<ApplicationUser>> CreateUserManagerMock()
    {
        var store = new Mock<IUserStore<ApplicationUser>>();
        return new Mock<UserManager<ApplicationUser>>(
            store.Object, null!, null!, null!, null!, null!, null!, null!, null!);
    }

    private static JsonDocument ParseJson(IActionResult result)
    {
        var json = Assert.IsType<JsonResult>(result);
        var serialized = JsonSerializer.Serialize(json.Value);
        return JsonDocument.Parse(serialized);
    }

    private static ApplicationDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        // Use a SuperAdmin user (no CompanyId claim) so that IsPlatformAdmin = true and the
        // global query filter evaluates to !IsDeleted only — not !IsDeleted && CompanyId == null,
        // which would filter out every row. DefaultHttpContext.Session throws, so we mock
        // HttpContext itself and stub Session.TryGetValue to return false (no impersonation).
        var identity = new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Role, "SuperAdmin") }, "Test");
        var principal = new ClaimsPrincipal(identity);

        byte[]? noBytes = null;
        var sessionMock = new Mock<ISession>();
        sessionMock.Setup(s => s.TryGetValue(It.IsAny<string>(), out noBytes)).Returns(false);

        var httpContextMock = new Mock<HttpContext>();
        httpContextMock.SetupGet(c => c.User).Returns(principal);
        httpContextMock.SetupGet(c => c.Session).Returns(sessionMock.Object);

        var accessor = new Mock<IHttpContextAccessor>();
        accessor.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object);

        return new ApplicationDbContext(options, accessor.Object, null!);
    }
}
