namespace Application.Tests.Students;

using Application.Students.DTOs;
using Application.Students.Queries;
using Domain.Exceptions;
using Domain.Ports.Repositories;
using FluentAssertions;
using NSubstitute;
using System.Linq.Expressions;
using Xunit;

public class GetStudentByIdQueryTests
{
    private readonly IStudentRepository _studentRepository;
    private readonly GetStudentByIdHandler _handler;

    public GetStudentByIdQueryTests()
    {
        _studentRepository = Substitute.For<IStudentRepository>();
        _handler = new GetStudentByIdHandler(_studentRepository);
    }

    [Fact]
    public async Task Handle_WhenStudentExists_ShouldReturnStudentDto()
    {
        // Arrange
        var expectedDto = new StudentDto(1, "John Doe", "john@example.com", 0, []);

        _studentRepository.GetByIdProjectedAsync(
            1,
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
            Arg.Any<CancellationToken>())
            .Returns(expectedDto);

        var query = new GetStudentByIdQuery(1);

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.Id.Should().Be(1);
        result.Name.Should().Be("John Doe");
        result.Email.Should().Be("john@example.com");
    }

    [Fact]
    public async Task Handle_WhenStudentNotFound_ShouldThrow()
    {
        // Arrange
        _studentRepository.GetByIdProjectedAsync<StudentDto>(
            999,
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
            Arg.Any<CancellationToken>())
            .Returns((StudentDto?)null);

        var query = new GetStudentByIdQuery(999);

        // Act
        var act = () => _handler.Handle(query, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<StudentNotFoundException>();
    }

    [Fact]
    public async Task Handle_WhenStudentHasEnrollments_ShouldReturnWithEnrollments()
    {
        // Arrange
        var enrollments = new List<EnrollmentDto>
        {
            new(1, 1, "Math", 3, "Prof A", DateTime.UtcNow),
            new(2, 2, "Physics", 3, "Prof B", DateTime.UtcNow)
        };
        var expectedDto = new StudentDto(1, "John Doe", "john@example.com", 6, enrollments);

        _studentRepository.GetByIdProjectedAsync(
            1,
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
            Arg.Any<CancellationToken>())
            .Returns(expectedDto);

        var query = new GetStudentByIdQuery(1);

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Enrollments.Should().HaveCount(2);
        result.TotalCredits.Should().Be(6);
    }
}

public class GetStudentsQueryTests
{
    private readonly IStudentRepository _studentRepository;
    private readonly GetStudentsHandler _handler;

    public GetStudentsQueryTests()
    {
        _studentRepository = Substitute.For<IStudentRepository>();
        _handler = new GetStudentsHandler(_studentRepository);
    }

    [Fact]
    public async Task Handle_ShouldReturnAllStudents()
    {
        // Arrange
        var students = new List<StudentDto>
        {
            new(1, "John Doe", "john@example.com", 0, []),
            new(2, "Jane Doe", "jane@example.com", 3, [])
        };

        _studentRepository.GetAllProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
            Arg.Any<CancellationToken>())
            .Returns(students);

        var query = new GetStudentsQuery();

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().HaveCount(2);
        result[0].Name.Should().Be("John Doe");
        result[1].Name.Should().Be("Jane Doe");
    }

    [Fact]
    public async Task Handle_WhenNoStudents_ShouldReturnEmptyList()
    {
        // Arrange
        _studentRepository.GetAllProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
            Arg.Any<CancellationToken>())
            .Returns(new List<StudentDto>());

        var query = new GetStudentsQuery();

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().BeEmpty();
    }
}

public class GetStudentsPagedQueryTests
{
    private readonly IStudentRepository _studentRepository;
    private readonly GetStudentsPagedHandler _handler;

    public GetStudentsPagedQueryTests()
    {
        _studentRepository = Substitute.For<IStudentRepository>();
        _handler = new GetStudentsPagedHandler(_studentRepository);
    }

    [Fact]
    public async Task Handle_ShouldReturnPagedResult()
    {
        // Arrange
        var items = new List<StudentPagedDto>
        {
            new(1, "John Doe", "john@example.com", 0),
            new(2, "Jane Doe", "jane@example.com", 3)
        };

        _studentRepository.GetPagedProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
            null,
            10,
            Arg.Any<CancellationToken>())
            .Returns((items, (int?)3, 5));

        var query = new GetStudentsPagedQuery();

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.Items.Should().HaveCount(2);
        result.NextCursor.Should().Be(3);
        result.TotalCount.Should().Be(5);
        result.HasNextPage.Should().BeTrue();
    }

    [Fact]
    public async Task Handle_WithCursor_ShouldPassCursorToRepository()
    {
        // Arrange
        _studentRepository.GetPagedProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
            5,
            10,
            Arg.Any<CancellationToken>())
            .Returns((new List<StudentPagedDto>(), (int?)null, 5));

        var query = new GetStudentsPagedQuery(AfterCursor: 5);

        // Act
        await _handler.Handle(query, CancellationToken.None);

        // Assert
        await _studentRepository.Received(1).GetPagedProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
            5,
            10,
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_WithLargePageSize_ShouldLimitTo50()
    {
        // Arrange
        _studentRepository.GetPagedProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
            Arg.Any<int?>(),
            50,
            Arg.Any<CancellationToken>())
            .Returns((new List<StudentPagedDto>(), (int?)null, 0));

        var query = new GetStudentsPagedQuery(PageSize: 100);

        // Act
        await _handler.Handle(query, CancellationToken.None);

        // Assert
        await _studentRepository.Received(1).GetPagedProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
            null,
            50,
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_WhenNoNextPage_ShouldReturnHasNextPageFalse()
    {
        // Arrange
        _studentRepository.GetPagedProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
            Arg.Any<int?>(),
            Arg.Any<int>(),
            Arg.Any<CancellationToken>())
            .Returns((new List<StudentPagedDto>(), (int?)null, 2));

        var query = new GetStudentsPagedQuery();

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        result.HasNextPage.Should().BeFalse();
        result.NextCursor.Should().BeNull();
    }
}
