<?php

declare(strict_types=1);

namespace App\Tests\Functional\Scolarite\Api;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
use App\Scolarite\Domain\Repository\EvaluationRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;

use const JSON_THROW_ON_ERROR;

use PHPUnit\Framework\Attributes\Test;
use Ramsey\Uuid\Uuid;

/**
 * Functional tests for competency mode endpoints (Story 6.5).
 *
 * Covers: competencies listing, competency levels, evaluation-competency linking,
 * competency result grids, saving results, and student progress tracking.
 */
final class CompetencyEndpointsTest extends ApiTestCase
{
    protected static ?bool $alwaysBootKernel = true;

    private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
    private const string TEACHER_ID = 'cc444444-4444-4444-4444-444444444444';
    private const string OTHER_TEACHER_ID = 'cc444444-4444-4444-4444-444444444445';
    private const string STUDENT_ID = 'cc222222-2222-2222-2222-222222222222';
    private const string STUDENT2_ID = 'cc222222-2222-2222-2222-222222222223';
    private const string CLASS_ID = 'cc555555-5555-5555-5555-555555555555';
    private const string SUBJECT_ID = 'cc666666-6666-6666-6666-666666666666';
    private const string FRAMEWORK_ID = 'cc777777-7777-7777-7777-777777777777';
    private const string COMPETENCY1_ID = 'cc888888-8888-8888-8888-888888888881';
    private const string COMPETENCY2_ID = 'cc888888-8888-8888-8888-888888888882';
    private const string BASE_URL = 'http://ecole-alpha.classeo.local/api';

    private ?EvaluationId $evaluationId = null;
    private ?string $competencyEvaluation1Id = null;
    private ?string $competencyEvaluation2Id = null;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seedFixtures();
    }

    protected function tearDown(): void
    {
        /** @var Connection $connection */
        $connection = static::getContainer()->get(Connection::class);

        $connection->executeStatement(
            'DELETE FROM student_competency_results WHERE competency_evaluation_id IN (SELECT id FROM competency_evaluations WHERE evaluation_id IN (SELECT id FROM evaluations WHERE tenant_id = :tid))',
            ['tid' => self::TENANT_ID],
        );
        $connection->executeStatement(
            'DELETE FROM competency_evaluations WHERE evaluation_id IN (SELECT id FROM evaluations WHERE tenant_id = :tid)',
            ['tid' => self::TENANT_ID],
        );
        $connection->executeStatement(
            'DELETE FROM competencies WHERE framework_id IN (SELECT id FROM competency_frameworks WHERE tenant_id = :tid)',
            ['tid' => self::TENANT_ID],
        );
        $connection->executeStatement(
            'DELETE FROM competency_frameworks WHERE tenant_id = :tid',
            ['tid' => self::TENANT_ID],
        );
        $connection->executeStatement(
            'DELETE FROM evaluations WHERE tenant_id = :tid',
            ['tid' => self::TENANT_ID],
        );

        parent::tearDown();
    }

    // =========================================================================
    // GET /competencies — Auth & Happy path
    // =========================================================================

    #[Test]
    public function getCompetenciesReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();
        $client->request('GET', self::BASE_URL . '/competencies', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    #[Test]
    public function getCompetenciesReturns200ForAuthenticatedUser(): void
    {
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('GET', self::BASE_URL . '/competencies', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{id: string, code: string, name: string}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        self::assertCount(2, $data);
        self::assertSame(self::COMPETENCY1_ID, $data[0]['id']);
        self::assertSame('C1', $data[0]['code']);
        self::assertSame('Lire et comprendre', $data[0]['name']);
        self::assertSame(self::COMPETENCY2_ID, $data[1]['id']);
        self::assertSame('C2', $data[1]['code']);
        self::assertSame('Ecrire', $data[1]['name']);
    }

    // =========================================================================
    // GET /competency-levels — Auth & Happy path
    // =========================================================================

    #[Test]
    public function getCompetencyLevelsReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();
        $client->request('GET', self::BASE_URL . '/competency-levels', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    #[Test]
    public function getCompetencyLevelsReturns200WithStandard4Levels(): void
    {
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('GET', self::BASE_URL . '/competency-levels', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{code: string, name: string}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        self::assertCount(4, $data);
        self::assertSame('not_acquired', $data[0]['code']);
        self::assertSame('Non acquis', $data[0]['name']);
        self::assertSame('in_progress', $data[1]['code']);
        self::assertSame('acquired', $data[2]['code']);
        self::assertSame('exceeded', $data[3]['code']);
    }

    // =========================================================================
    // GET /evaluations/{id}/competencies — Auth & edge cases
    // =========================================================================

    #[Test]
    public function getEvaluationCompetenciesReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();
        $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    #[Test]
    public function getEvaluationCompetenciesReturns403ForNonOwner(): void
    {
        $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']);
        $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(403);
    }

    #[Test]
    public function getEvaluationCompetenciesReturns404ForUnknownEvaluation(): void
    {
        $unknownId = (string) EvaluationId::generate();
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('GET', self::BASE_URL . '/evaluations/' . $unknownId . '/competencies', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(404);
    }

    #[Test]
    public function getEvaluationCompetenciesReturns200ForOwner(): void
    {
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{id: string, competencyId: string, competencyCode: string}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        self::assertCount(2, $data);
        self::assertSame((string) $this->evaluationId, $data[0]['evaluationId']);
        self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']);
        self::assertSame('C1', $data[0]['competencyCode']);
        self::assertSame(self::COMPETENCY2_ID, $data[1]['competencyId']);
        self::assertSame('C2', $data[1]['competencyCode']);
    }

    // =========================================================================
    // GET /evaluations/{id}/competency-results — Auth & Happy path
    // =========================================================================

    #[Test]
    public function getCompetencyResultsReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();
        $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    #[Test]
    public function getCompetencyResultsReturns403ForNonOwner(): void
    {
        $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']);
        $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(403);
    }

    #[Test]
    public function getCompetencyResultsReturns200ForOwnerWithGrid(): void
    {
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{studentId: string, competencyEvaluationId: string, levelCode: string|null}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        // 2 students x 2 competencies = 4 rows in the result matrix
        self::assertCount(4, $data);

        // Find the seeded result (student1 + competency1 = acquired)
        $seededResult = null;
        foreach ($data as $row) {
            if ($row['studentId'] === self::STUDENT_ID && $row['competencyEvaluationId'] === $this->competencyEvaluation1Id) {
                $seededResult = $row;
                break;
            }
        }

        self::assertNotNull($seededResult);
        self::assertSame('acquired', $seededResult['levelCode']);
    }

    // =========================================================================
    // PUT /evaluations/{id}/competency-results — Auth & Happy path
    // =========================================================================

    #[Test]
    public function putCompetencyResultsReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();
        $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'results' => [],
            ],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    #[Test]
    public function putCompetencyResultsReturns403ForNonOwner(): void
    {
        $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']);
        $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'results' => [],
            ],
        ]);

        self::assertResponseStatusCodeSame(403);
    }

    #[Test]
    public function putCompetencyResultsReturns200AndSavesResults(): void
    {
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'results' => [
                    [
                        'studentId' => self::STUDENT2_ID,
                        'competencyEvaluationId' => $this->competencyEvaluation1Id,
                        'levelCode' => 'in_progress',
                    ],
                    [
                        'studentId' => self::STUDENT2_ID,
                        'competencyEvaluationId' => $this->competencyEvaluation2Id,
                        'levelCode' => 'not_acquired',
                    ],
                ],
            ],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{studentId: string, levelCode: string}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        self::assertCount(2, $data);
        self::assertSame(self::STUDENT2_ID, $data[0]['studentId']);
        self::assertSame('in_progress', $data[0]['levelCode']);
        self::assertSame(self::STUDENT2_ID, $data[1]['studentId']);
        self::assertSame('not_acquired', $data[1]['levelCode']);
    }

    // =========================================================================
    // POST /evaluations/{id}/competencies — Auth & Happy path
    // =========================================================================

    #[Test]
    public function postLinkCompetenciesReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();
        $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'competencyIds' => [self::COMPETENCY1_ID],
            ],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    #[Test]
    public function postLinkCompetenciesReturns403ForNonOwner(): void
    {
        $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']);
        $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'competencyIds' => [self::COMPETENCY1_ID],
            ],
        ]);

        self::assertResponseStatusCodeSame(403);
    }

    #[Test]
    public function postLinkCompetenciesReturns200AndLinksCompetencies(): void
    {
        // First, remove existing links to test fresh linking
        /** @var Connection $connection */
        $connection = static::getContainer()->get(Connection::class);
        $connection->executeStatement(
            'DELETE FROM student_competency_results WHERE competency_evaluation_id IN (SELECT id FROM competency_evaluations WHERE evaluation_id = :eid)',
            ['eid' => (string) $this->evaluationId],
        );
        $connection->executeStatement(
            'DELETE FROM competency_evaluations WHERE evaluation_id = :eid',
            ['eid' => (string) $this->evaluationId],
        );

        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'competencyIds' => [self::COMPETENCY1_ID, self::COMPETENCY2_ID],
            ],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{evaluationId: string, competencyId: string}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        self::assertCount(2, $data);
        self::assertSame((string) $this->evaluationId, $data[0]['evaluationId']);
        self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']);
        self::assertSame(self::COMPETENCY2_ID, $data[1]['competencyId']);
    }

    // =========================================================================
    // PUT /evaluations/{id}/competency-results — Validation (error paths)
    // =========================================================================

    #[Test]
    public function putCompetencyResultsReturns400ForInvalidLevelCode(): void
    {
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'results' => [
                    [
                        'studentId' => self::STUDENT_ID,
                        'competencyEvaluationId' => $this->competencyEvaluation1Id,
                        'levelCode' => 'INVALID_CODE',
                    ],
                ],
            ],
        ]);

        self::assertResponseStatusCodeSame(400);
    }

    #[Test]
    public function putCompetencyResultsReturns400ForStudentNotInClass(): void
    {
        $outsiderId = 'cc999999-9999-9999-9999-999999999999';

        /** @var Connection $connection */
        $connection = static::getContainer()->get(Connection::class);
        $connection->executeStatement(
            "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at)
             VALUES (:id, :tid, 'outsider-comp@test.local', '', 'Outsider', 'User', '[\"ROLE_ELEVE\"]', 'active', NOW(), NOW())
             ON CONFLICT (id) DO NOTHING",
            ['id' => $outsiderId, 'tid' => self::TENANT_ID],
        );

        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'results' => [
                    [
                        'studentId' => $outsiderId,
                        'competencyEvaluationId' => $this->competencyEvaluation1Id,
                        'levelCode' => 'acquired',
                    ],
                ],
            ],
        ]);

        self::assertResponseStatusCodeSame(400);
    }

    #[Test]
    public function putCompetencyResultsReturns400ForInvalidCompetencyEvaluationId(): void
    {
        $fakeCeId = (string) Uuid::uuid4();

        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'results' => [
                    [
                        'studentId' => self::STUDENT_ID,
                        'competencyEvaluationId' => $fakeCeId,
                        'levelCode' => 'acquired',
                    ],
                ],
            ],
        ]);

        self::assertResponseStatusCodeSame(400);
    }

    #[Test]
    public function putCompetencyResultsDeletesResultWhenLevelCodeIsNull(): void
    {
        // Verify the seeded result exists via the results grid
        $getClient = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $getClient->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $getClient->getResponse()->getContent();
        /** @var array<array{studentId: string, competencyEvaluationId: string, levelCode: string|null}> $before */
        $before = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
        $seededBefore = null;
        foreach ($before as $row) {
            if ($row['studentId'] === self::STUDENT_ID && $row['competencyEvaluationId'] === $this->competencyEvaluation1Id) {
                $seededBefore = $row;
                break;
            }
        }
        self::assertNotNull($seededBefore);
        self::assertSame('acquired', $seededBefore['levelCode']);

        // Send null levelCode to delete the result
        $putClient = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $putClient->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'results' => [
                    [
                        'studentId' => self::STUDENT_ID,
                        'competencyEvaluationId' => $this->competencyEvaluation1Id,
                        'levelCode' => null,
                    ],
                ],
            ],
        ]);

        self::assertResponseIsSuccessful();

        // Verify the result is now null in the grid
        $verifyClient = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $verifyClient->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        /** @var string $afterContent */
        $afterContent = $verifyClient->getResponse()->getContent();
        /** @var array<array{studentId: string, competencyEvaluationId: string, levelCode: string|null}> $after */
        $after = json_decode($afterContent, true, 512, JSON_THROW_ON_ERROR);
        $seededAfter = null;
        foreach ($after as $row) {
            if ($row['studentId'] === self::STUDENT_ID && $row['competencyEvaluationId'] === $this->competencyEvaluation1Id) {
                $seededAfter = $row;
                break;
            }
        }
        self::assertNotNull($seededAfter);
        self::assertNull($seededAfter['levelCode'] ?? null);
    }

    // =========================================================================
    // POST /evaluations/{id}/competencies — Validation (error paths)
    // =========================================================================

    #[Test]
    public function postLinkCompetenciesReturns400ForUnknownCompetencyId(): void
    {
        $fakeCompetencyId = (string) Uuid::uuid4();

        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'competencyIds' => [$fakeCompetencyId],
            ],
        ]);

        self::assertResponseStatusCodeSame(400);
    }

    #[Test]
    public function postLinkCompetenciesIsIdempotentForAlreadyLinkedCompetency(): void
    {
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);

        // Link the same competencies that are already linked
        $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'competencyIds' => [self::COMPETENCY1_ID],
            ],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{competencyId: string}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        self::assertCount(1, $data);
        self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']);
    }

    // =========================================================================
    // AC6: Mode mixte — Competency results don't create grades
    // =========================================================================

    #[Test]
    public function competencyResultsDoNotCreateGradeEntries(): void
    {
        /** @var Connection $connection */
        $connection = static::getContainer()->get(Connection::class);

        // Count grades before saving competency results
        /** @var string $countBefore */
        $countBefore = $connection->fetchOne(
            'SELECT COUNT(*) FROM grades WHERE evaluation_id = :eid',
            ['eid' => (string) $this->evaluationId],
        );

        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [
            'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
            'json' => [
                'results' => [
                    [
                        'studentId' => self::STUDENT_ID,
                        'competencyEvaluationId' => $this->competencyEvaluation1Id,
                        'levelCode' => 'exceeded',
                    ],
                    [
                        'studentId' => self::STUDENT2_ID,
                        'competencyEvaluationId' => $this->competencyEvaluation2Id,
                        'levelCode' => 'in_progress',
                    ],
                ],
            ],
        ]);

        self::assertResponseIsSuccessful();

        // Count grades after — should be unchanged (competency results don't create grades)
        /** @var string $countAfter */
        $countAfter = $connection->fetchOne(
            'SELECT COUNT(*) FROM grades WHERE evaluation_id = :eid',
            ['eid' => (string) $this->evaluationId],
        );

        self::assertSame($countBefore, $countAfter, 'Saving competency results must not create grade entries');

        // Also verify no student_averages were created for this evaluation
        /** @var string $avgCount */
        $avgCount = $connection->fetchOne(
            'SELECT COUNT(*) FROM student_averages WHERE tenant_id = :tid AND subject_id = :sid',
            ['tid' => self::TENANT_ID, 'sid' => self::SUBJECT_ID],
        );

        self::assertSame(0, (int) $avgCount, 'Competency results must not trigger average calculation');
    }

    // =========================================================================
    // GET /students/{id}/competency-progress — Auth & Happy path
    // =========================================================================

    #[Test]
    public function getStudentCompetencyProgressReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();
        $client->request('GET', self::BASE_URL . '/students/' . self::STUDENT_ID . '/competency-progress', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    #[Test]
    public function getStudentCompetencyProgressReturns403ForOtherStudent(): void
    {
        $client = $this->createAuthenticatedClient(self::STUDENT2_ID, ['ROLE_ELEVE']);
        $client->request('GET', self::BASE_URL . '/students/' . self::STUDENT_ID . '/competency-progress', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseStatusCodeSame(403);
    }

    #[Test]
    public function getStudentCompetencyProgressReturns200ForOwnData(): void
    {
        $client = $this->createAuthenticatedClient(self::STUDENT_ID, ['ROLE_ELEVE']);
        $client->request('GET', self::BASE_URL . '/students/' . self::STUDENT_ID . '/competency-progress', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{competencyId: string, currentLevelCode: string}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        self::assertCount(1, $data);
        self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']);
        self::assertSame('acquired', $data[0]['currentLevelCode']);
        self::assertSame('Acquis', $data[0]['currentLevelName']);
        self::assertNotEmpty($data[0]['history']);
    }

    #[Test]
    public function getStudentCompetencyProgressReturns200ForTeacher(): void
    {
        $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']);
        $client->request('GET', self::BASE_URL . '/students/' . self::STUDENT_ID . '/competency-progress', [
            'headers' => ['Accept' => 'application/json'],
        ]);

        self::assertResponseIsSuccessful();

        /** @var string $content */
        $content = $client->getResponse()->getContent();
        /** @var array<array{competencyId: string}> $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        self::assertCount(1, $data);
        self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']);
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    /**
     * @param list<string> $roles
     */
    private function createAuthenticatedClient(string $userId, array $roles): Client
    {
        $client = static::createClient();

        $user = new SecurityUser(
            userId: UserId::fromString($userId),
            email: 'test-comp@classeo.local',
            hashedPassword: '',
            tenantId: TenantId::fromString(self::TENANT_ID),
            roles: $roles,
        );

        $client->loginUser($user, 'api');

        return $client;
    }

    private function seedFixtures(): void
    {
        $container = static::getContainer();
        /** @var Connection $connection */
        $connection = $container->get(Connection::class);
        $tenantId = TenantId::fromString(self::TENANT_ID);
        $now = new DateTimeImmutable();

        $schoolId = '550e8400-e29b-41d4-a716-ff6655440001';
        $academicYearId = '550e8400-e29b-41d4-a716-ff6655440002';

        // --- Users ---
        $connection->executeStatement(
            "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at)
             VALUES (:id, :tid, 'teacher-comp@test.local', '', 'Test', 'Teacher', '[\"ROLE_PROF\"]', 'active', NOW(), NOW())
             ON CONFLICT (id) DO NOTHING",
            ['id' => self::TEACHER_ID, 'tid' => self::TENANT_ID],
        );
        $connection->executeStatement(
            "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at)
             VALUES (:id, :tid, 'other-teacher-comp@test.local', '', 'Other', 'Teacher', '[\"ROLE_PROF\"]', 'active', NOW(), NOW())
             ON CONFLICT (id) DO NOTHING",
            ['id' => self::OTHER_TEACHER_ID, 'tid' => self::TENANT_ID],
        );
        $connection->executeStatement(
            "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at)
             VALUES (:id, :tid, 'student1-comp@test.local', '', 'Alice', 'Dupont', '[\"ROLE_ELEVE\"]', 'active', NOW(), NOW())
             ON CONFLICT (id) DO NOTHING",
            ['id' => self::STUDENT_ID, 'tid' => self::TENANT_ID],
        );
        $connection->executeStatement(
            "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at)
             VALUES (:id, :tid, 'student2-comp@test.local', '', 'Bob', 'Martin', '[\"ROLE_ELEVE\"]', 'active', NOW(), NOW())
             ON CONFLICT (id) DO NOTHING",
            ['id' => self::STUDENT2_ID, 'tid' => self::TENANT_ID],
        );

        // --- School class ---
        $connection->executeStatement(
            "INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, status, created_at, updated_at)
             VALUES (:id, :tid, :sid, :ayid, 'Test-Comp-Class', 'active', NOW(), NOW())
             ON CONFLICT (id) DO NOTHING",
            ['id' => self::CLASS_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId, 'ayid' => $academicYearId],
        );

        // --- Subject ---
        $connection->executeStatement(
            "INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at)
             VALUES (:id, :tid, :sid, 'Test-Comp-Subject', 'TCOMP', 'active', NOW(), NOW())
             ON CONFLICT (id) DO NOTHING",
            ['id' => self::SUBJECT_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId],
        );

        // --- Class assignments (students assigned to class) ---
        $classAssignment1Id = (string) Uuid::uuid4();
        $connection->executeStatement(
            'INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at)
             VALUES (:id, :tid, :uid, :cid, :ayid, NOW(), NOW(), NOW())
             ON CONFLICT (user_id, academic_year_id) DO NOTHING',
            ['id' => $classAssignment1Id, 'tid' => self::TENANT_ID, 'uid' => self::STUDENT_ID, 'cid' => self::CLASS_ID, 'ayid' => $academicYearId],
        );
        $classAssignment2Id = (string) Uuid::uuid4();
        $connection->executeStatement(
            'INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at)
             VALUES (:id, :tid, :uid, :cid, :ayid, NOW(), NOW(), NOW())
             ON CONFLICT (user_id, academic_year_id) DO NOTHING',
            ['id' => $classAssignment2Id, 'tid' => self::TENANT_ID, 'uid' => self::STUDENT2_ID, 'cid' => self::CLASS_ID, 'ayid' => $academicYearId],
        );

        // --- Evaluation (owned by teacher) ---
        $evaluation = Evaluation::creer(
            tenantId: $tenantId,
            classId: ClassId::fromString(self::CLASS_ID),
            subjectId: SubjectId::fromString(self::SUBJECT_ID),
            teacherId: UserId::fromString(self::TEACHER_ID),
            title: 'Evaluation Competences',
            description: null,
            evaluationDate: new DateTimeImmutable('2026-03-15'),
            gradeScale: new GradeScale(20),
            coefficient: new Coefficient(1.0),
            now: $now,
        );
        $evaluation->pullDomainEvents();

        /** @var EvaluationRepository $evalRepo */
        $evalRepo = $container->get(EvaluationRepository::class);
        $evalRepo->save($evaluation);

        $this->evaluationId = $evaluation->id;

        // --- Competency framework (default for tenant) ---
        $connection->executeStatement(
            'INSERT INTO competency_frameworks (id, tenant_id, name, is_default, created_at)
             VALUES (:id, :tid, :name, true, NOW())
             ON CONFLICT (id) DO NOTHING',
            ['id' => self::FRAMEWORK_ID, 'tid' => self::TENANT_ID, 'name' => 'Socle commun'],
        );

        // --- Competencies ---
        $connection->executeStatement(
            'INSERT INTO competencies (id, framework_id, code, name, description, parent_id, sort_order)
             VALUES (:id, :fid, :code, :name, :desc, NULL, :sort)
             ON CONFLICT (id) DO NOTHING',
            ['id' => self::COMPETENCY1_ID, 'fid' => self::FRAMEWORK_ID, 'code' => 'C1', 'name' => 'Lire et comprendre', 'desc' => 'Capacite de lecture', 'sort' => 1],
        );
        $connection->executeStatement(
            'INSERT INTO competencies (id, framework_id, code, name, description, parent_id, sort_order)
             VALUES (:id, :fid, :code, :name, :desc, NULL, :sort)
             ON CONFLICT (id) DO NOTHING',
            ['id' => self::COMPETENCY2_ID, 'fid' => self::FRAMEWORK_ID, 'code' => 'C2', 'name' => 'Ecrire', 'desc' => 'Capacite d\'ecriture', 'sort' => 2],
        );

        // --- Competency evaluations (link competencies to evaluation) ---
        $this->competencyEvaluation1Id = (string) Uuid::uuid4();
        $connection->executeStatement(
            'INSERT INTO competency_evaluations (id, evaluation_id, competency_id)
             VALUES (:id, :eid, :cid)
             ON CONFLICT (evaluation_id, competency_id) DO NOTHING',
            ['id' => $this->competencyEvaluation1Id, 'eid' => (string) $this->evaluationId, 'cid' => self::COMPETENCY1_ID],
        );
        $this->competencyEvaluation2Id = (string) Uuid::uuid4();
        $connection->executeStatement(
            'INSERT INTO competency_evaluations (id, evaluation_id, competency_id)
             VALUES (:id, :eid, :cid)
             ON CONFLICT (evaluation_id, competency_id) DO NOTHING',
            ['id' => $this->competencyEvaluation2Id, 'eid' => (string) $this->evaluationId, 'cid' => self::COMPETENCY2_ID],
        );

        // --- Student competency result (student1 + competency1 = acquired) ---
        $resultId = (string) Uuid::uuid4();
        $connection->executeStatement(
            'INSERT INTO student_competency_results (id, tenant_id, competency_evaluation_id, student_id, level_code, created_at, updated_at)
             VALUES (:id, :tid, :ceid, :sid, :level, NOW(), NOW())
             ON CONFLICT (competency_evaluation_id, student_id) DO NOTHING',
            [
                'id' => $resultId,
                'tid' => self::TENANT_ID,
                'ceid' => $this->competencyEvaluation1Id,
                'sid' => self::STUDENT_ID,
                'level' => 'acquired',
            ],
        );
    }
}
