namespace Application.Tests.Subjects;

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

public class GetSubjectsQueryTests
{
    private readonly ISubjectRepository _subjectRepository;
    private readonly GetSubjectsHandler _handler;

    public GetSubjectsQueryTests()
    {
        _subjectRepository = Substitute.For<ISubjectRepository>();
        _handler = new GetSubjectsHandler(_subjectRepository);
    }

    [Fact]
    public async Task Handle_ShouldReturnAllSubjects()
    {
        // Arrange
        var subjects = new List<SubjectDto>
        {
            new(1, "Math", 3, 1, "Prof A"),
            new(2, "Physics", 3, 1, "Prof A"),
            new(3, "Chemistry", 3, 2, "Prof B")
        };

        _subjectRepository.GetAllProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Subject, SubjectDto>>>(),
            Arg.Any<CancellationToken>())
            .Returns(subjects);

        var query = new GetSubjectsQuery();

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

        // Assert
        result.Should().HaveCount(3);
        result[0].Name.Should().Be("Math");
        result[0].ProfessorName.Should().Be("Prof A");
    }

    [Fact]
    public async Task Handle_WhenNoSubjects_ShouldReturnEmptyList()
    {
        // Arrange
        _subjectRepository.GetAllProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Subject, SubjectDto>>>(),
            Arg.Any<CancellationToken>())
            .Returns(new List<SubjectDto>());

        var query = new GetSubjectsQuery();

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

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

    [Fact]
    public async Task Handle_ShouldReturnCorrectCredits()
    {
        // Arrange
        var subjects = new List<SubjectDto>
        {
            new(1, "Math", 3, 1, "Prof A")
        };

        _subjectRepository.GetAllProjectedAsync(
            Arg.Any<Expression<Func<Domain.Entities.Subject, SubjectDto>>>(),
            Arg.Any<CancellationToken>())
            .Returns(subjects);

        var query = new GetSubjectsQuery();

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

        // Assert
        result[0].Credits.Should().Be(3);
    }
}

public class GetAvailableSubjectsQueryTests
{
    private readonly IStudentRepository _studentRepository;
    private readonly ISubjectRepository _subjectRepository;
    private readonly GetAvailableSubjectsHandler _handler;

    public GetAvailableSubjectsQueryTests()
    {
        _studentRepository = Substitute.For<IStudentRepository>();
        _subjectRepository = Substitute.For<ISubjectRepository>();
        _handler = new GetAvailableSubjectsHandler(_studentRepository, _subjectRepository);
    }

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

        var query = new GetAvailableSubjectsQuery(999);

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

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

    [Fact]
    public async Task Handle_WhenNoEnrollments_AllSubjectsShouldBeAvailable()
    {
        // Arrange
        var student = new Domain.Entities.Student("John", Domain.ValueObjects.Email.Create("john@test.com"));

        var professor = new Domain.Entities.Professor("Prof A");
        SetEntityId(professor, 1);

        var subjects = new List<Domain.Entities.Subject>
        {
            CreateSubject(1, "Math", 1, professor),
            CreateSubject(2, "Physics", 1, professor)
        };

        _studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
            .Returns(student);
        _subjectRepository.GetAllWithProfessorsAsync(Arg.Any<CancellationToken>())
            .Returns(subjects);

        var query = new GetAvailableSubjectsQuery(1);

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

        // Assert
        result.Should().HaveCount(2);
        result.Should().OnlyContain(s => s.IsAvailable);
    }

    [Fact]
    public async Task Handle_WhenAlreadyEnrolled_SubjectShouldNotBeAvailable()
    {
        // Arrange
        var student = new Domain.Entities.Student("John", Domain.ValueObjects.Email.Create("john@test.com"));
        SetEntityId(student, 1);

        var professor = new Domain.Entities.Professor("Prof A");
        SetEntityId(professor, 1);

        var subject = CreateSubject(1, "Math", 1, professor);

        var enrollment = new Domain.Entities.Enrollment(1, 1);
        SetNavigationProperty(enrollment, "Subject", subject);
        student.AddEnrollment(enrollment);

        _studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
            .Returns(student);
        _subjectRepository.GetAllWithProfessorsAsync(Arg.Any<CancellationToken>())
            .Returns(new List<Domain.Entities.Subject> { subject });

        var query = new GetAvailableSubjectsQuery(1);

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

        // Assert
        result[0].IsAvailable.Should().BeFalse();
        result[0].UnavailableReason.Should().Be("Already enrolled");
    }

    [Fact]
    public async Task Handle_WhenSameProfessor_SubjectShouldNotBeAvailable()
    {
        // Arrange
        var student = new Domain.Entities.Student("John", Domain.ValueObjects.Email.Create("john@test.com"));
        SetEntityId(student, 1);

        var professor = new Domain.Entities.Professor("Prof A");
        SetEntityId(professor, 1);

        var subject1 = CreateSubject(1, "Math", 1, professor);
        var subject2 = CreateSubject(2, "Physics", 1, professor);

        var enrollment = new Domain.Entities.Enrollment(1, 1);
        SetNavigationProperty(enrollment, "Subject", subject1);
        student.AddEnrollment(enrollment);

        _studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
            .Returns(student);
        _subjectRepository.GetAllWithProfessorsAsync(Arg.Any<CancellationToken>())
            .Returns(new List<Domain.Entities.Subject> { subject1, subject2 });

        var query = new GetAvailableSubjectsQuery(1);

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

        // Assert
        var physics = result.First(s => s.Name == "Physics");
        physics.IsAvailable.Should().BeFalse();
        physics.UnavailableReason.Should().Be("Already have a subject with this professor");
    }

    [Fact]
    public async Task Handle_WhenMaxEnrollmentsReached_AllSubjectsShouldBeUnavailable()
    {
        // Arrange
        var student = new Domain.Entities.Student("John", Domain.ValueObjects.Email.Create("john@test.com"));
        SetEntityId(student, 1);

        var prof1 = new Domain.Entities.Professor("Prof A");
        var prof2 = new Domain.Entities.Professor("Prof B");
        var prof3 = new Domain.Entities.Professor("Prof C");
        var prof4 = new Domain.Entities.Professor("Prof D");
        SetEntityId(prof1, 1);
        SetEntityId(prof2, 2);
        SetEntityId(prof3, 3);
        SetEntityId(prof4, 4);

        var subject1 = CreateSubject(1, "Math", 1, prof1);
        var subject2 = CreateSubject(2, "Physics", 2, prof2);
        var subject3 = CreateSubject(3, "Chemistry", 3, prof3);
        var subject4 = CreateSubject(4, "Biology", 4, prof4);

        student.AddEnrollment(CreateEnrollmentWithSubject(1, 1, subject1));
        student.AddEnrollment(CreateEnrollmentWithSubject(1, 2, subject2));
        student.AddEnrollment(CreateEnrollmentWithSubject(1, 3, subject3));

        _studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
            .Returns(student);
        _subjectRepository.GetAllWithProfessorsAsync(Arg.Any<CancellationToken>())
            .Returns(new List<Domain.Entities.Subject> { subject4 });

        var query = new GetAvailableSubjectsQuery(1);

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

        // Assert
        result[0].IsAvailable.Should().BeFalse();
        result[0].UnavailableReason.Should().Be("Maximum 3 subjects reached");
    }

    private static void SetEntityId<T>(T entity, int id) where T : class
    {
        typeof(T).GetProperty("Id")?.SetValue(entity, id);
    }

    private static void SetNavigationProperty<T>(T entity, string propertyName, object value) where T : class
    {
        typeof(T).GetProperty(propertyName)?.SetValue(entity, value);
    }

    private static Domain.Entities.Subject CreateSubject(int id, string name, int professorId, Domain.Entities.Professor professor)
    {
        var subject = new Domain.Entities.Subject(name, professorId);
        SetEntityId(subject, id);
        SetNavigationProperty(subject, "Professor", professor);
        return subject;
    }

    private static Domain.Entities.Enrollment CreateEnrollmentWithSubject(int studentId, int subjectId, Domain.Entities.Subject subject)
    {
        var enrollment = new Domain.Entities.Enrollment(studentId, subjectId);
        SetNavigationProperty(enrollment, "Subject", subject);
        return enrollment;
    }
}
