namespace Application.Tests.Enrollments;

using Application.Enrollments.Commands;
using Domain.Entities;
using Domain.Exceptions;
using Domain.Ports.Repositories;
using Domain.Services;
using Domain.ValueObjects;
using FluentAssertions;
using NSubstitute;
using Xunit;

public class EnrollStudentCommandTests
{
    private readonly IStudentRepository _studentRepo = Substitute.For<IStudentRepository>();
    private readonly ISubjectRepository _subjectRepo = Substitute.For<ISubjectRepository>();
    private readonly IEnrollmentRepository _enrollmentRepo = Substitute.For<IEnrollmentRepository>();
    private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
    private readonly EnrollmentDomainService _enrollmentService = new();
    private readonly EnrollStudentHandler _sut;

    public EnrollStudentCommandTests()
    {
        _sut = new EnrollStudentHandler(
            _studentRepo,
            _subjectRepo,
            _enrollmentRepo,
            _enrollmentService,
            _unitOfWork);
    }

    [Fact]
    public async Task Handle_WhenValid_ShouldEnrollStudent()
    {
        var student = CreateStudent();
        var subject = CreateSubjectWithProfessor(1, "Math", 1, "Prof. Smith");

        _studentRepo.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
            .Returns(student);
        _subjectRepo.GetByIdWithProfessorAsync(1, Arg.Any<CancellationToken>())
            .Returns(subject);

        var command = new EnrollStudentCommand(1, 1);
        var result = await _sut.Handle(command, CancellationToken.None);

        result.Should().NotBeNull();
        result.SubjectName.Should().Be("Math");
        result.ProfessorName.Should().Be("Prof. Smith");

        _enrollmentRepo.Received(1).Add(Arg.Any<Enrollment>());
        await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
    }

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

        var command = new EnrollStudentCommand(999, 1);
        var act = () => _sut.Handle(command, CancellationToken.None);

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

    [Fact]
    public async Task Handle_WhenSubjectNotFound_ShouldThrow()
    {
        var student = CreateStudent();
        _studentRepo.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
            .Returns(student);
        _subjectRepo.GetByIdWithProfessorAsync(999, Arg.Any<CancellationToken>())
            .Returns((Subject?)null);

        var command = new EnrollStudentCommand(1, 999);
        var act = () => _sut.Handle(command, CancellationToken.None);

        await act.Should().ThrowAsync<SubjectNotFoundException>();
    }

    [Fact]
    public async Task Handle_WhenMaxEnrollmentsReached_ShouldThrow()
    {
        var student = CreateStudentWithEnrollments(3);
        var subject = CreateSubjectWithProfessor(10, "Physics", 99, "New Prof");

        _studentRepo.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
            .Returns(student);
        _subjectRepo.GetByIdWithProfessorAsync(10, Arg.Any<CancellationToken>())
            .Returns(subject);

        var command = new EnrollStudentCommand(1, 10);
        var act = () => _sut.Handle(command, CancellationToken.None);

        await act.Should().ThrowAsync<MaxEnrollmentsExceededException>();
        _enrollmentRepo.DidNotReceive().Add(Arg.Any<Enrollment>());
    }

    [Fact]
    public async Task Handle_WhenSameProfessor_ShouldThrow()
    {
        var student = CreateStudent();
        var existingSubject = CreateSubjectWithProfessor(1, "Math", 1, "Prof. Smith");
        AddEnrollmentToStudent(student, existingSubject);

        var newSubject = CreateSubjectWithProfessor(2, "Algebra", 1, "Prof. Smith");

        _studentRepo.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
            .Returns(student);
        _subjectRepo.GetByIdWithProfessorAsync(2, Arg.Any<CancellationToken>())
            .Returns(newSubject);

        var command = new EnrollStudentCommand(1, 2);
        var act = () => _sut.Handle(command, CancellationToken.None);

        await act.Should().ThrowAsync<SameProfessorConstraintException>();
    }

    private static Student CreateStudent()
    {
        var student = new Student("John Doe", Email.Create("john@test.com"));
        typeof(Student).GetProperty("Id")!.SetValue(student, 1);
        return student;
    }

    private static Student CreateStudentWithEnrollments(int count)
    {
        var student = CreateStudent();
        for (int i = 0; i < count; i++)
        {
            var subject = CreateSubjectWithProfessor(i + 1, $"Subject{i}", i + 1, $"Prof{i}");
            AddEnrollmentToStudent(student, subject);
        }
        return student;
    }

    private static Subject CreateSubjectWithProfessor(int id, string name, int profId, string profName)
    {
        var professor = new Professor(profName);
        typeof(Professor).GetProperty("Id")!.SetValue(professor, profId);

        var subject = new Subject(name, profId);
        typeof(Subject).GetProperty("Id")!.SetValue(subject, id);
        typeof(Subject).GetProperty("Professor")!.SetValue(subject, professor);

        return subject;
    }

    private static void AddEnrollmentToStudent(Student student, Subject subject)
    {
        var enrollment = new Enrollment(student.Id, subject.Id);
        typeof(Enrollment).GetProperty("Subject")!.SetValue(enrollment, subject);
        student.AddEnrollment(enrollment);
    }
}
