namespace Domain.Tests.Services;

using Domain.Entities;
using Domain.Exceptions;
using Domain.Services;
using Domain.ValueObjects;
using FluentAssertions;
using Xunit;

public class EnrollmentDomainServiceTests
{
    private readonly EnrollmentDomainService _sut = new();
    private static Email ValidEmail => Email.Create("test@example.com");

    [Fact]
    public void ValidateEnrollment_WhenValid_ShouldNotThrow()
    {
        var student = new Student("John Doe", ValidEmail);
        var subject = new Subject("Math", professorId: 1);

        var act = () => _sut.ValidateEnrollment(student, subject);

        act.Should().NotThrow();
    }

    [Fact]
    public void ValidateEnrollment_WhenMaxEnrollmentsReached_ShouldThrow()
    {
        var student = new Student("John Doe", ValidEmail);
        AddEnrollmentsWithDifferentProfessors(student, 3);
        var subject = new Subject("Physics", professorId: 99);

        var act = () => _sut.ValidateEnrollment(student, subject);

        act.Should().Throw<MaxEnrollmentsExceededException>()
            .Which.Code.Should().Be("MAX_ENROLLMENTS");
    }

    [Fact]
    public void ValidateEnrollment_WhenSameProfessor_ShouldThrow()
    {
        var student = new Student("John Doe", ValidEmail);
        var existingSubject = new Subject("Math", professorId: 1);
        AddEnrollmentWithSubject(student, existingSubject);

        var newSubject = new Subject("Algebra", professorId: 1);

        var act = () => _sut.ValidateEnrollment(student, newSubject);

        act.Should().Throw<SameProfessorConstraintException>()
            .Which.Code.Should().Be("SAME_PROFESSOR");
    }

    [Fact]
    public void ValidateEnrollment_WhenDuplicateSubject_ShouldThrow()
    {
        var student = new Student("John Doe", ValidEmail);
        var subject = new Subject("Math", professorId: 1);
        SetSubjectId(subject, 10);
        AddEnrollmentWithSubject(student, subject);

        // Same subject ID but different professor to bypass professor check
        var duplicateSubject = new Subject("Math", professorId: 2);
        SetSubjectId(duplicateSubject, 10);

        var act = () => _sut.ValidateEnrollment(student, duplicateSubject);

        act.Should().Throw<DuplicateEnrollmentException>()
            .Which.Code.Should().Be("DUPLICATE_ENROLLMENT");
    }

    [Fact]
    public void CreateEnrollment_WhenValid_ShouldCreateAndAddToStudent()
    {
        var student = new Student("John Doe", ValidEmail);
        var subject = new Subject("Math", professorId: 1);

        var enrollment = _sut.CreateEnrollment(student, subject);

        enrollment.Should().NotBeNull();
        student.Enrollments.Should().Contain(enrollment);
        enrollment.StudentId.Should().Be(student.Id);
        enrollment.SubjectId.Should().Be(subject.Id);
    }

    [Fact]
    public void CreateEnrollment_WhenInvalid_ShouldNotAddToStudent()
    {
        var student = new Student("John Doe", ValidEmail);
        AddEnrollmentsWithDifferentProfessors(student, 3);
        var subject = new Subject("Physics", professorId: 99);

        var act = () => _sut.CreateEnrollment(student, subject);

        act.Should().Throw<MaxEnrollmentsExceededException>();
        student.Enrollments.Should().HaveCount(3);
    }

    private static void AddEnrollmentsWithDifferentProfessors(Student student, int count)
    {
        for (int i = 0; i < count; i++)
        {
            var subject = new Subject($"Subject{i}", professorId: i + 1);
            SetSubjectId(subject, i + 1);
            AddEnrollmentWithSubject(student, subject);
        }
    }

    private static void AddEnrollmentWithSubject(Student student, Subject subject)
    {
        var enrollment = new Enrollment(student.Id, subject.Id);
        SetSubjectOnEnrollment(enrollment, subject);
        student.AddEnrollment(enrollment);
    }

    private static void SetSubjectOnEnrollment(Enrollment enrollment, Subject subject)
    {
        typeof(Enrollment).GetProperty("Subject")!.SetValue(enrollment, subject);
    }

    private static void SetSubjectId(Subject subject, int id)
    {
        typeof(Subject).GetProperty("Id")!.SetValue(subject, id);
    }
}
