namespace Integration.Tests;

using Adapters.Driven.Persistence.Context;
using Adapters.Driven.Persistence.Repositories;
using Application.Enrollments.Commands;
using Application.Students.Commands;
using Domain.Entities;
using Domain.Exceptions;
using Domain.Services;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;

public class EnrollmentFlowTests : IDisposable
{
    private readonly AppDbContext _context;
    private readonly StudentRepository _studentRepository;
    private readonly SubjectRepository _subjectRepository;
    private readonly EnrollmentRepository _enrollmentRepository;
    private readonly UnitOfWork _unitOfWork;

    public EnrollmentFlowTests()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;

        _context = new AppDbContext(options);
        _studentRepository = new StudentRepository(_context);
        _subjectRepository = new SubjectRepository(_context);
        _enrollmentRepository = new EnrollmentRepository(_context);
        _unitOfWork = new UnitOfWork(_context);

        SeedData();
    }

    private void SeedData()
    {
        var prof1 = new Professor("Dr. Smith");
        var prof2 = new Professor("Dr. Jones");
        _context.Professors.AddRange(prof1, prof2);
        _context.SaveChanges();

        var subjects = new[]
        {
            new Subject("Mathematics", prof1.Id),
            new Subject("Physics", prof1.Id),
            new Subject("Chemistry", prof2.Id),
            new Subject("Biology", prof2.Id)
        };
        _context.Subjects.AddRange(subjects);
        _context.SaveChanges();
    }

    [Fact]
    public async Task CreateStudent_ShouldPersistToDatabase()
    {
        // Arrange
        var handler = new CreateStudentHandler(_studentRepository, _unitOfWork);
        var command = new CreateStudentCommand("John Doe", "john@example.com");

        // Act
        var result = await handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.Name.Should().Be("John Doe");

        var savedStudent = await _context.Students.FirstOrDefaultAsync(s => s.Email.Value == "john@example.com");
        savedStudent.Should().NotBeNull();
    }

    [Fact]
    public async Task EnrollStudent_ShouldCreateEnrollment()
    {
        // Arrange
        var student = new Student("Jane Doe", Domain.ValueObjects.Email.Create("jane@example.com"));
        _context.Students.Add(student);
        await _context.SaveChangesAsync();

        var subject = await _context.Subjects.FirstAsync();
        var enrollmentService = new EnrollmentDomainService();
        var handler = new EnrollStudentHandler(
            _studentRepository, _subjectRepository, _enrollmentRepository,
            enrollmentService, _unitOfWork);

        var command = new EnrollStudentCommand(student.Id, subject.Id);

        // Act
        var result = await handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();

        var enrollment = await _context.Enrollments
            .FirstOrDefaultAsync(e => e.StudentId == student.Id && e.SubjectId == subject.Id);
        enrollment.Should().NotBeNull();
    }

    [Fact]
    public async Task EnrollStudent_WhenMaxEnrollments_ShouldThrow()
    {
        // Arrange
        var student = new Student("Max Student", Domain.ValueObjects.Email.Create("max@example.com"));
        _context.Students.Add(student);
        await _context.SaveChangesAsync();

        var subjects = await _context.Subjects.Take(4).ToListAsync();
        var enrollmentService = new EnrollmentDomainService();

        // Enroll in 3 subjects (max allowed) with different professors
        var subjectsWithDiffProfs = subjects.GroupBy(s => s.ProfessorId).Select(g => g.First()).Take(3).ToList();

        if (subjectsWithDiffProfs.Count < 3)
        {
            // If we don't have 3 different professors, just use first 3 subjects
            subjectsWithDiffProfs = subjects.Take(3).ToList();
        }

        foreach (var subject in subjectsWithDiffProfs.Take(2))
        {
            _context.Enrollments.Add(new Enrollment(student.Id, subject.Id));
        }
        await _context.SaveChangesAsync();

        // Need to reload student with enrollments
        _context.Entry(student).State = EntityState.Detached;

        var handler = new EnrollStudentHandler(
            _studentRepository, _subjectRepository, _enrollmentRepository,
            enrollmentService, _unitOfWork);

        // Add third enrollment
        var thirdSubject = subjectsWithDiffProfs.Skip(2).First();
        var thirdCommand = new EnrollStudentCommand(student.Id, thirdSubject.Id);
        await handler.Handle(thirdCommand, CancellationToken.None);

        // Try fourth enrollment - should fail
        var fourthSubject = subjects.First(s => !subjectsWithDiffProfs.Contains(s));
        var fourthCommand = new EnrollStudentCommand(student.Id, fourthSubject.Id);

        // Act
        var act = () => handler.Handle(fourthCommand, CancellationToken.None);

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

    [Fact]
    public async Task EnrollStudent_WhenSameProfessor_ShouldThrow()
    {
        // Arrange
        var student = new Student("Prof Test", Domain.ValueObjects.Email.Create("prof@example.com"));
        _context.Students.Add(student);
        await _context.SaveChangesAsync();

        // Get all subjects and find two with same professor in memory
        var allSubjects = await _context.Subjects.ToListAsync();
        var subjectsSameProf = allSubjects
            .GroupBy(s => s.ProfessorId)
            .Where(g => g.Count() >= 2)
            .SelectMany(g => g.Take(2))
            .ToList();

        if (subjectsSameProf.Count < 2)
        {
            // Skip test if we don't have subjects with same professor
            return;
        }

        var enrollmentService = new EnrollmentDomainService();
        var handler = new EnrollStudentHandler(
            _studentRepository, _subjectRepository, _enrollmentRepository,
            enrollmentService, _unitOfWork);

        // Enroll first subject
        var firstCommand = new EnrollStudentCommand(student.Id, subjectsSameProf[0].Id);
        await handler.Handle(firstCommand, CancellationToken.None);

        // Try second subject with same professor
        var secondCommand = new EnrollStudentCommand(student.Id, subjectsSameProf[1].Id);

        // Act
        var act = () => handler.Handle(secondCommand, CancellationToken.None);

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

    [Fact]
    public async Task DeleteStudent_ShouldRemoveFromDatabase()
    {
        // Arrange
        var student = new Student("Delete Me", Domain.ValueObjects.Email.Create("delete@example.com"));
        _context.Students.Add(student);
        await _context.SaveChangesAsync();
        var studentId = student.Id;

        var handler = new DeleteStudentHandler(_studentRepository, _unitOfWork);
        var command = new DeleteStudentCommand(studentId);

        // Act
        var result = await handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().BeTrue();
        var deletedStudent = await _context.Students.FindAsync(studentId);
        deletedStudent.Should().BeNull();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}
