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

namespace PowderCoating.UnitTests;

public class CustomersControllerCrmTests
{
    // ── Details — guard ───────────────────────────────────────────────────

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

        var result = await controller.Details(id: 999);

        Assert.IsType<NotFoundResult>(result);
    }

    // ── Details — zero-history customer ───────────────────────────────────

    [Fact]
    public async Task Details_WithNoHistory_ProducesZeroStats()
    {
        await using var context = CreateContext();
        SeedCustomer(context, id: 1, companyId: 1);
        await context.SaveChangesAsync();

        var controller = CreateController(context, companyId: 1);
        var result = await controller.Details(id: 1);

        var view = Assert.IsType<ViewResult>(result);
        var stats = view.ViewData["CrmStats"] as CustomerLifetimeStatsDto;

        Assert.NotNull(stats);
        Assert.Equal(0, stats.TotalJobs);
        Assert.Equal(0, stats.TotalQuotes);
        Assert.Equal(0m, stats.TotalRevenue);
        Assert.Equal(0m, stats.AverageJobValue);   // no divide-by-zero
        Assert.Null(stats.DaysSinceLastJob);
    }

    [Fact]
    public async Task Details_WithNoHistory_DoesNotRenderTimeline()
    {
        await using var context = CreateContext();
        SeedCustomer(context, id: 1, companyId: 1);
        await context.SaveChangesAsync();

        var controller = CreateController(context, companyId: 1);
        var result = await controller.Details(id: 1);

        var view = Assert.IsType<ViewResult>(result);
        var timeline = view.ViewData["Timeline"] as List<CustomerTimelineEventDto>;

        Assert.NotNull(timeline);
        Assert.Empty(timeline);
    }

    // ── Details — stats calculation ───────────────────────────────────────

    [Fact]
    public async Task Details_WithJobsAndInvoices_CalculatesStatsCorrectly()
    {
        await using var context = CreateContext();
        SeedCustomer(context, id: 1, companyId: 1);
        var activeStatus = SeedJobStatus(context, id: 1, isTerminal: false);
        SeedJob(context, id: 1, customerId: 1, companyId: 1, statusId: activeStatus.Id, finalPrice: 300m);
        SeedJob(context, id: 2, customerId: 1, companyId: 1, statusId: activeStatus.Id, finalPrice: 700m);
        SeedInvoice(context, id: 1, customerId: 1, companyId: 1, total: 300m, amountPaid: 300m, status: InvoiceStatus.Paid);
        SeedInvoice(context, id: 2, customerId: 1, companyId: 1, total: 700m, amountPaid: 400m, status: InvoiceStatus.PartiallyPaid);
        await context.SaveChangesAsync();

        var controller = CreateController(context, companyId: 1);
        var result = await controller.Details(id: 1);

        var view = Assert.IsType<ViewResult>(result);
        var stats = view.ViewData["CrmStats"] as CustomerLifetimeStatsDto;

        Assert.NotNull(stats);
        Assert.Equal(2, stats.TotalJobs);
        Assert.Equal(2, stats.ActiveJobs);
        Assert.Equal(2, stats.TotalInvoices);
        Assert.Equal(1000m, stats.TotalRevenue);
        Assert.Equal(700m, stats.TotalCollected);
        Assert.Equal(500m, stats.AverageJobValue);
    }

    // ── Details — voided invoices excluded ────────────────────────────────

    [Fact]
    public async Task Details_VoidedInvoicesExcludedFromRevenueAndCollected()
    {
        await using var context = CreateContext();
        SeedCustomer(context, id: 1, companyId: 1);
        var status = SeedJobStatus(context, id: 1, isTerminal: false);
        SeedJob(context, id: 1, customerId: 1, companyId: 1, statusId: status.Id, finalPrice: 500m);
        SeedInvoice(context, id: 1, customerId: 1, companyId: 1, total: 500m, amountPaid: 500m, status: InvoiceStatus.Paid);
        SeedInvoice(context, id: 2, customerId: 1, companyId: 1, total: 999m, amountPaid: 0m,   status: InvoiceStatus.Voided);
        await context.SaveChangesAsync();

        var controller = CreateController(context, companyId: 1);
        var result = await controller.Details(id: 1);

        var view = Assert.IsType<ViewResult>(result);
        var stats = view.ViewData["CrmStats"] as CustomerLifetimeStatsDto;

        Assert.NotNull(stats);
        Assert.Equal(500m, stats.TotalRevenue);    // voided invoice excluded
        Assert.Equal(500m, stats.TotalCollected);   // voided invoice excluded
        Assert.Equal(2, stats.TotalInvoices);       // count includes voided (informational)
    }

    // ── Details — active-job count excludes terminal statuses ─────────────

    [Fact]
    public async Task Details_ActiveJobsExcludesTerminalStatuses()
    {
        await using var context = CreateContext();
        SeedCustomer(context, id: 1, companyId: 1);
        var active    = SeedJobStatus(context, id: 1, isTerminal: false);
        var completed = SeedJobStatus(context, id: 2, isTerminal: true);
        SeedJob(context, id: 1, customerId: 1, companyId: 1, statusId: active.Id,    finalPrice: 100m);
        SeedJob(context, id: 2, customerId: 1, companyId: 1, statusId: completed.Id, finalPrice: 200m);
        SeedJob(context, id: 3, customerId: 1, companyId: 1, statusId: completed.Id, finalPrice: 300m);
        await context.SaveChangesAsync();

        var controller = CreateController(context, companyId: 1);
        var result = await controller.Details(id: 1);

        var view = Assert.IsType<ViewResult>(result);
        var stats = view.ViewData["CrmStats"] as CustomerLifetimeStatsDto;

        Assert.NotNull(stats);
        Assert.Equal(3, stats.TotalJobs);
        Assert.Equal(1, stats.ActiveJobs);
    }

    // ── Details — timeline cap and sort ───────────────────────────────────

    [Fact]
    public async Task Details_TimelineCappedAt15Events()
    {
        await using var context = CreateContext();
        SeedCustomer(context, id: 1, companyId: 1);
        var status = SeedJobStatus(context, id: 1, isTerminal: false);

        for (int i = 1; i <= 18; i++)
        {
            context.Jobs.Add(new Job
            {
                Id          = i,
                CompanyId   = 1,
                CustomerId  = 1,
                JobStatusId = status.Id,
                JobNumber   = $"JOB-0001-{i:D4}",
                Description = $"Job {i}",
                FinalPrice  = 100m,
                CreatedAt   = new DateTime(2026, 1, i, 0, 0, 0, DateTimeKind.Utc)
            });
        }
        await context.SaveChangesAsync();

        var controller = CreateController(context, companyId: 1);
        var result = await controller.Details(id: 1);

        var view = Assert.IsType<ViewResult>(result);
        var timeline = view.ViewData["Timeline"] as List<CustomerTimelineEventDto>;

        Assert.NotNull(timeline);
        Assert.Equal(15, timeline.Count);
    }

    [Fact]
    public async Task Details_TimelineIsSortedNewestFirst()
    {
        await using var context = CreateContext();
        SeedCustomer(context, id: 1, companyId: 1);

        // Use Invoice.InvoiceDate for timeline dates — SaveChangesAsync stamps CreatedAt but
        // does not touch InvoiceDate, so we can seed distinct values that survive the save.
        SeedInvoice(context, id: 1, customerId: 1, companyId: 1, total: 100m, amountPaid: 100m,
            status: InvoiceStatus.Paid, invoiceDate: new DateTime(2026, 3, 1, 0, 0, 0, DateTimeKind.Utc));
        SeedInvoice(context, id: 2, customerId: 1, companyId: 1, total: 200m, amountPaid: 0m,
            status: InvoiceStatus.Sent, invoiceDate: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
        SeedInvoice(context, id: 3, customerId: 1, companyId: 1, total: 300m, amountPaid: 0m,
            status: InvoiceStatus.Sent, invoiceDate: new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc));
        await context.SaveChangesAsync();

        var controller = CreateController(context, companyId: 1);
        var result = await controller.Details(id: 1);

        var view = Assert.IsType<ViewResult>(result);
        var timeline = view.ViewData["Timeline"] as List<CustomerTimelineEventDto>;

        Assert.NotNull(timeline);
        Assert.Equal(3, timeline.Count);
        Assert.True(timeline[0].Date > timeline[1].Date, "First event should be the newest");
        Assert.True(timeline[1].Date > timeline[2].Date, "Events should be descending");
    }

    // ── Details — tenant isolation ────────────────────────────────────────

    [Fact]
    public async Task Details_DoesNotIncludeJobsFromOtherCompanies()
    {
        await using var context = CreateContext();
        SeedCustomer(context, id: 1, companyId: 1);
        var status = SeedJobStatus(context, id: 1, isTerminal: false);
        SeedJob(context, id: 1, customerId: 1, companyId: 1, statusId: status.Id, finalPrice: 500m);  // this company
        SeedJob(context, id: 2, customerId: 1, companyId: 2, statusId: status.Id, finalPrice: 999m);  // other company
        await context.SaveChangesAsync();

        var controller = CreateController(context, companyId: 1);
        var result = await controller.Details(id: 1);

        var view = Assert.IsType<ViewResult>(result);
        var stats = view.ViewData["CrmStats"] as CustomerLifetimeStatsDto;

        Assert.NotNull(stats);
        Assert.Equal(1, stats.TotalJobs);
        Assert.Equal(500m, stats.AverageJobValue);
    }

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

    private static CustomersController CreateController(ApplicationDbContext context, int companyId)
    {
        var uow = new UnitOfWork(context);

        // Tests cover CrmStats/Timeline logic, not DTO mapping — stub mapper to return a valid model
        var mapperMock = new Mock<IMapper>();
        mapperMock
            .Setup(m => m.Map<CustomerDto>(It.IsAny<Customer>()))
            .Returns((Customer c) => new CustomerDto { Id = c.Id, CompanyName = c.CompanyName, IsActive = c.IsActive });
        var mapper = mapperMock.Object;

        var tenantContext = new Mock<ITenantContext>();
        tenantContext.Setup(t => t.GetCurrentCompanyId()).Returns(companyId);

        var controller = new CustomersController(
            uow,
            mapper,
            Mock.Of<ILogger<CustomersController>>(),
            Mock.Of<INotificationService>(),
            Mock.Of<ISubscriptionService>(),
            tenantContext.Object,
            CreateUserManagerMock().Object,
            Mock.Of<IFinancialReportService>());

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

        return controller;
    }

    private static void SeedCustomer(ApplicationDbContext context, int id, int companyId)
    {
        context.Customers.Add(new Customer
        {
            Id          = id,
            CompanyId   = companyId,
            CompanyName = $"Test Customer {id}",
            IsActive    = true,
            CreatedAt   = DateTime.UtcNow
        });
    }

    private static JobStatusLookup SeedJobStatus(ApplicationDbContext context, int id, bool isTerminal)
    {
        var status = new JobStatusLookup
        {
            Id               = id,
            StatusCode       = isTerminal ? "COMPLETED" : "IN_PROGRESS",
            DisplayName      = isTerminal ? "Completed" : "In Progress",
            DisplayOrder     = id,
            IsTerminalStatus = isTerminal
        };
        context.JobStatusLookups.Add(status);
        return status;
    }

    private static void SeedJob(
        ApplicationDbContext context, int id, int customerId, int companyId, int statusId, decimal finalPrice)
    {
        context.Jobs.Add(new Job
        {
            Id          = id,
            CompanyId   = companyId,
            CustomerId  = customerId,
            JobStatusId = statusId,
            JobNumber   = $"JOB-0001-{id:D4}",
            Description = $"Job {id}",
            FinalPrice  = finalPrice,
            CreatedAt   = DateTime.UtcNow
        });
    }

    private static Job MakeJob(int id, int customerId, int companyId, int statusId, DateTime date) =>
        new Job
        {
            Id          = id,
            CompanyId   = companyId,
            CustomerId  = customerId,
            JobStatusId = statusId,
            JobNumber   = $"JOB-0001-{id:D4}",
            Description = $"Job {id}",
            FinalPrice  = 100m,
            CreatedAt   = date
        };

    private static void SeedInvoice(
        ApplicationDbContext context, int id, int customerId, int companyId,
        decimal total, decimal amountPaid, InvoiceStatus status,
        DateTime? invoiceDate = null)
    {
        context.Invoices.Add(new Invoice
        {
            Id            = id,
            CompanyId     = companyId,
            CustomerId    = customerId,
            InvoiceNumber = $"INV-0001-{id:D4}",
            InvoiceDate   = invoiceDate ?? DateTime.UtcNow,
            Total         = total,
            AmountPaid    = amountPaid,
            Status        = status
        });
    }

    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 ApplicationDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        // SuperAdmin principal: bypasses the CompanyId global query filter so all
        // seeded rows are visible, matching the same approach in DepositsControllerTests.
        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!);
    }
}
