<?php declare(strict_types=1);

namespace App\Tests\Unit\Service;

use PHPUnit\Framework\Attributes\DataProvider;
use App\DataFixtures\Test\NonSortOrderTeamCategoryFixture;
use App\DataFixtures\Test\TeamWithExternalIdEqualsOneFixture;
use App\DataFixtures\Test\TeamWithExternalIdEqualsTwoFixture;
use App\DataTransferObject\ResultRow;
use App\Entity\Contest;
use App\Entity\ContestProblem;
use App\Entity\ExternalContestSourceType;
use App\Entity\Language;
use App\Entity\Problem;
use App\Entity\ScoreboardType;
use App\Entity\Team;
use App\Entity\TeamAffiliation;
use App\Entity\TeamCategory;
use App\Entity\User;
use App\Service\DOMJudgeService;
use App\Service\ImportExportService;
use App\Service\ScoreboardService;
use App\Tests\Unit\BaseTestCase;
use DateInterval;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface;
use Generator;
use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
use Symfony\Component\Serializer\SerializerInterface;

class ImportExportServiceTest extends BaseTestCase
{
    protected function setUp(): void
    {
        self::bootKernel();
    }

    #[DataProvider('provideImportContestDataErrors')]
    public function testImportContestDataErrors(mixed $data, string $expectedMessage): void
    {
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        self::assertFalse($importExportService->importContestData($data, $message, $cid));
        self::assertEquals($expectedMessage, $message);
        self::assertNull($cid);
    }

    public static function provideImportContestDataErrors(): Generator
    {
        yield [[], 'Error parsing YAML file.'];
        yield [['name' => 'Some name'], 'Missing fields: one of (start_time, start-time), one of (id, short_name, short-name), duration'];
        yield [['short-name' => 'somename', 'start-time' => '2020-01-01 12:34:56'], 'Missing fields: one of (name, formal_name), duration'];
        yield [
            [
                'name'       => 'Test contest',
                'short-name' => 'test',
                'duration'   => '5:00:00',
                'start-time' => 'Invalid start time here',
            ],
            'Can not parse start-time'
        ];
        yield [
            [
                'name'       => 'Test contest',
                'id'         => 'test',
                'duration'   => '5:00:00',
                'start_time' => 'Invalid start time here',
            ],
            'Can not parse start_time'
        ];
        yield [
            [
                'name'                     => 'Test contest',
                'short-name'               => 'test',
                'duration'                 => '5:00:00',
                'start-time'               => '2020-01-01T12:34:56+02:00',
                'scoreboard-freeze-length' => '6:00:00',
            ],
            'Freeze duration is longer than contest length'
        ];
        yield [
            [
                'name'                       => 'Test contest',
                'id'                         => 'test',
                'duration'                   => '5:00:00',
                'start_time'                 => '2020-01-01T12:34:56+02:00',
                'scoreboard_freeze_duration' => '6:00:00',
            ],
            'Freeze duration is longer than contest length'
        ];
        yield [
            [
                'name'                     => '',
                'short-name'               => '',
                'duration'                 => '5:00:00',
                'start-time'               => '2020-01-01T12:34:56+02:00',
                'scoreboard-freeze-length' => '30:00',
            ],
            "Contest has errors:\n\n  • `name`: This value should not be blank.\n  • `shortname`: This value should not be blank."
        ];
    }

    #[DataProvider('provideImportContestDataSuccess')]
    public function testImportContestDataSuccess(
        mixed $data,
        string $expectedShortName,
        string $expectedActivateTimeString,
        ?string $expectedDeactivateTimeString,
        array $expectedProblems = [],
        int $expectedPenaltyTime = 20,
    ): void {
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        self::assertTrue($importExportService->importContestData($data, $message, $cid), 'Importing failed: ' . $message);
        self::assertNull($message);
        self::assertIsString($cid);

        $contest = $this->getContest($cid);

        self::assertEquals($data['name'], $contest->getName());
        self::assertEquals($data['public'] ?? true, $contest->getPublic());
        self::assertEquals($expectedShortName, $contest->getShortname());
        self::assertEquals($expectedActivateTimeString, $contest->getActivatetimeString());
        self::assertEquals($expectedDeactivateTimeString, $contest->getDeactivatetimeString());
        self::assertEquals($expectedPenaltyTime, $contest->getPenaltyTime());

        if (isset($data['scoreboard_type']) || isset($data['scoreboard-type'])) {
            self::assertEquals($data['scoreboard_type'] ?? $data['scoreboard-type'], $contest->getScoreboardType()->value);
        }

        $problems = [];
        /** @var ContestProblem $contestProblem */
        foreach ($contest->getProblems() as $contestProblem) {
            $problems[$contestProblem->getShortname()] = $contestProblem->getProblem()->getExternalid();

            // Verify problem types match the contest scoreboard type.
            if ($contest->getScoreboardType() === ScoreboardType::SCORE) {
                self::assertTrue($contestProblem->getProblem()->isScoringProblem(),
                    sprintf('Problem %s should be scoring type for a scoring contest', $contestProblem->getShortname()));
            } else {
                self::assertTrue($contestProblem->getProblem()->isPassFailProblem(),
                    sprintf('Problem %s should be pass-fail type for a pass-fail contest', $contestProblem->getShortname()));
            }
        }

        self::assertEquals($expectedProblems, $problems);
    }

    public static function provideImportContestDataSuccess(): Generator
    {
        // YAML format:

        // Simple case
        yield [
            [
                'name'                     => 'Some test contest',
                'short-name'               => 'test-contest',
                'duration'                 => '5:00:00',
                'start-time'               => '2020-01-01T12:34:56+02:00',
                'scoreboard-freeze-length' => '1:00:00',
            ],
            'test-contest',
            '2020-01-01 10:34:56 UTC',
            null,
        ];
        // Adding absolute activate and deactivate time
        yield [
            [
                'name'                     => 'Some test contest',
                'short-name'               => 'test-contest',
                'duration'                 => '5:00:00',
                'start-time'               => '2020-01-01T12:34:56+02:00',
                'activate_time'            => '2020-01-01T06:34:56+02:00',
                'deactivate_time'          => '2020-01-01T18:34:56+02:00',
                'scoreboard-freeze-length' => '1:00:00',
            ],
            'test-contest',
            '2020-01-01 04:34:56 UTC',
            '2020-01-01 16:34:56 UTC',
        ];
        // Adding relative activate and deactivate time
        yield [
            [
                'name'                     => 'Some test contest',
                'short-name'               => 'test-contest',
                'duration'                 => '5:00:00',
                'start-time'               => '2020-01-01T12:34:56+02:00',
                'activate_time'            => '-6:00',
                'deactivate_time'          => '+06:00:00',
                'scoreboard-freeze-length' => '1:00:00',
            ],
            'test-contest',
            '-6:00',
            '+06:00:00',
        ];
        // - Freeze length without hours
        // - Set a short name with invalid characters
        // - Use DateTime object for start time
        yield [
            [
                'name'                     => 'Some test contest',
                'short-name'               => 'test-contest $-@ test',
                'duration'                 => '5:00:00',
                'start-time'               => new DateTime('2020-01-01T12:34:56+02:00'),
                'scoreboard-freeze-length' => '30:00',
            ],
            'test-contest__-__test',
            '2020-01-01 10:34:56 UTC',
            null,
        ];
        // Real life example from NWERC 2020 practice session, including problems.
        yield [
            [
                'duration'                 => '2:00:00',
                'name'                     => 'NWERC 2020 Practice Session',
                'penalty-time'             => '20',
                'scoreboard-freeze-length' => '30:00',
                'short-name'               => 'practice',
                'start-time'               => '2021-03-27 09:00:00+00:00',
                'public'                   => true,
                'problems'                 => [
                    [
                        'color'      => '#FE9DAF',
                        'letter'     => 'A',
                        'rgb'        => '#FE9DAF',
                        'short-name' => 'anothereruption',
                    ],
                    [
                        'color'      => '#008100',
                        'letter'     => 'B',
                        'rgb'        => '#008100',
                        'short-name' => 'brokengears',
                    ],
                    [
                        'color'      => '#FF7109',
                        'letter'     => 'C',
                        'rgb'        => '#FF7109',
                        'short-name' => 'cheating',
                    ],
                ],
            ],
            'practice',
            '2021-03-27 09:00:00 UTC',
            null,
            ['A' => 'anothereruption', 'B' => 'brokengears', 'C' => 'cheating'],
        ];

        // JSON (API) format:
        yield [
            [
                'name'                       => 'Some test contest',
                'id'                         => 'test-contest',
                'duration'                   => '5:00:00',
                'start_time'                 => '2020-01-01T12:34:56+02:00',
                'scoreboard_freeze_duration' => '1:00:00',
                'public'                     => false,
            ],
            'test-contest',
            '2020-01-01 10:34:56 UTC',
            null,
        ];
        // Testing scoreboard-type (with hyphen)
        yield [
            [
                'name'            => 'Scoreboard Type Test',
                'short-name'      => 'score-test',
                'duration'        => '5:00:00',
                'start-time'      => '2020-01-01T12:34:56+02:00',
                'scoreboard-type' => 'score',
            ],
            'score-test',
            '2020-01-01 10:34:56 UTC',
            null,
            [],
            0,
        ];
        // Testing different penalty time
        yield [
            [
                'name'            => 'Penalty Type Test',
                'short-name'      => 'penalty-time-test',
                'duration'        => '5:00:00',
                'start-time'      => '2020-01-01T12:34:56+02:00',
                'penalty-time'    => '123456',
            ],
            'penalty-time-test',
            '2020-01-01 10:34:56 UTC',
            null,
            [],
            123456,
        ];
        // Scoring contest with problems: verify problems get scoring type.
        yield [
            [
                'name'             => 'Scoring Contest With Problems',
                'short-name'       => 'scoring-problems',
                'duration'         => '5:00:00',
                'start-time'       => '2020-01-01T12:34:56+02:00',
                'scoreboard_type'  => 'score',
                'problems'         => [
                    [
                        'letter'     => 'A',
                        'short-name' => 'scoreprobA',
                        'color'      => '#FF0000',
                    ],
                    [
                        'letter'     => 'B',
                        'short-name' => 'scoreprobB',
                        'color'      => '#00FF00',
                    ],
                ],
            ],
            'scoring-problems',
            '2020-01-01 10:34:56 UTC',
            null,
            ['A' => 'scoreprobA', 'B' => 'scoreprobB'],
            0,
        ];
    }

    #[DataProvider('provideImportContestDataWithShadow')]
    public function testImportContestDataWithShadow(
        mixed $data,
        bool $expectedEnabled,
        bool $expectedUseJudgements,
        ?ExternalContestSourceType $expectedType,
        ?string $expectedSource
    ): void {
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        self::assertTrue($importExportService->importContestData($data, $message, $cid), 'Importing failed: ' . $message);
        self::assertNull($message);
        self::assertIsString($cid);

        $contest = $this->getContest($cid);

        self::assertEquals($expectedEnabled, $contest->isExternalSourceEnabled());
        self::assertEquals($expectedUseJudgements, $contest->isExternalSourceUseJudgements());
        self::assertEquals($expectedType, $contest->getExternalSourceType());
        self::assertEquals($expectedSource, $contest->getExternalSourceSource());
    }

    public static function provideImportContestDataWithShadow(): Generator
    {
        // Presence of shadow key enables shadow mode automatically
        yield [
            [
                'name'       => 'Shadow contest CCS API',
                'short-name' => 'shadow-ccs',
                'duration'   => '5:00:00',
                'start-time' => '2020-01-01T12:34:56+02:00',
                'shadow'     => [
                    'type'   => 'ccs-api',
                    'source' => 'https://example.com/api/contests/test',
                ],
            ],
            true,
            false,
            ExternalContestSourceType::CCS_API,
            'https://example.com/api/contests/test',
        ];
        // With useJudgements enabled
        yield [
            [
                'name'       => 'Shadow contest with judgements',
                'short-name' => 'shadow-judge',
                'duration'   => '5:00:00',
                'start-time' => '2020-01-01T12:34:56+02:00',
                'shadow'     => [
                    'type'           => 'ccs-api',
                    'source'         => 'https://example.com/api/contests/test',
                    'use_judgements' => true,
                ],
            ],
            true,
            true,
            ExternalContestSourceType::CCS_API,
            'https://example.com/api/contests/test',
        ];
        // Contest package type
        yield [
            [
                'name'       => 'Shadow contest package',
                'short-name' => 'shadow-pkg',
                'duration'   => '5:00:00',
                'start-time' => '2020-01-01T12:34:56+02:00',
                'shadow'     => [
                    'type'   => 'contest-archive',
                    'source' => '/path/to/contest',
                ],
            ],
            true,
            false,
            ExternalContestSourceType::CONTEST_PACKAGE,
            '/path/to/contest',
        ];
        // Contest without shadow key - shadow mode should be disabled
        yield [
            [
                'name'       => 'Normal contest',
                'short-name' => 'normal',
                'duration'   => '5:00:00',
                'start-time' => '2020-01-01T12:34:56+02:00',
            ],
            false,
            false,
            null,
            null,
        ];
    }

    #[DataProvider('provideExportContestYamlDataWithShadow')]
    public function testExportContestYamlDataWithShadow(
        bool $enabled,
        bool $useJudgements,
        ?ExternalContestSourceType $type,
        ?string $source,
        ?string $username,
        ?string $password,
        ?array $expectedShadow
    ): void {
        // First create a contest
        $contestData = [
            'name'       => 'Export test contest',
            'short-name' => 'export-test',
            'duration'   => '5:00:00',
            'start-time' => '2020-01-01T12:34:56+02:00',
        ];
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        self::assertTrue($importExportService->importContestData($contestData, $message, $cid), 'Importing failed: ' . $message);

        $contest = $this->getContest($cid);
        $contest->setExternalSourceEnabled($enabled);
        $contest->setExternalSourceUseJudgements($useJudgements);
        $contest->setExternalSourceType($type);
        $contest->setExternalSourceSource($source);
        $contest->setExternalSourceUsername($username);
        $contest->setExternalSourcePassword($password);

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);
        $em->flush();

        $data = $importExportService->getContestYamlData($contest, false);

        if ($expectedShadow === null) {
            self::assertArrayNotHasKey('shadow', $data);
        } else {
            self::assertArrayHasKey('shadow', $data);
            self::assertEquals($expectedShadow, $data['shadow']);
        }
    }

    public static function provideExportContestYamlDataWithShadow(): Generator
    {
        // Shadow mode disabled - no shadow key in export
        yield [
            false,
            false,
            null,
            null,
            null,
            null,
            null,
        ];
        // Shadow mode enabled with CCS API and all fields
        yield [
            true,
            true,
            ExternalContestSourceType::CCS_API,
            'https://example.com/api/contests/test',
            'admin',
            'secret123',
            [
                'type'           => 'ccs-api',
                'source'         => 'https://example.com/api/contests/test',
                'use_judgements' => true,
                'username'       => 'admin',
                'password'       => 'secret123', // ggignore
            ],
        ];
        // Shadow mode enabled with contest archive, no useJudgements
        yield [
            true,
            false,
            ExternalContestSourceType::CONTEST_PACKAGE,
            '/path/to/archive',
            null,
            null,
            [
                'type'   => 'contest-archive',
                'source' => '/path/to/archive',
            ],
        ];
        // Shadow mode enabled with CCS API, only username (no password)
        yield [
            true,
            false,
            ExternalContestSourceType::CCS_API,
            'https://example.com/api/contests/test',
            'readonly',
            null,
            [
                'type'     => 'ccs-api',
                'source'   => 'https://example.com/api/contests/test',
                'username' => 'readonly',
            ],
        ];
    }

    #[DataProvider('provideImportProblemsDataSuccess')]
    public function testImportProblemsDataSuccess(mixed $data, array $expectedProblems): void
    {
        // First create a new contest by import it
        $contestData = [
            'name'                       => 'Some test contest',
            'id'                         => 'test-contest',
            'duration'                   => '5:00:00',
            'start_time'                 => '2020-01-01T12:34:56+02:00',
            'scoreboard_freeze_duration' => '1:00:00',
        ];
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importExportService->importContestData($contestData, $message, $cid);

        $contest = $this->getContest($cid);
        self::assertTrue($importExportService->importProblemsData($contest, $data, $ids));
        self::assertNotNull($ids);
        self::assertCount(count($expectedProblems), $ids);

        $contest = $this->getContest($cid);

        $problems = [];
        /** @var ContestProblem $problem */
        foreach ($contest->getProblems() as $problem) {
            $problems[$problem->getShortname()] = [
                'name'       => $problem->getProblem()->getName(),
                'externalid' => $problem->getProblem()->getExternalid(),
                'timelimit'  => $problem->getProblem()->getTimelimit(),
                'color'      => $problem->getColor(),
            ];
        }

        self::assertEquals($expectedProblems, $problems);
    }

    public static function provideImportProblemsDataSuccess(): Generator
    {
        yield [
            [
                [
                    'color'      => '#FE9DAF',
                    'letter'     => 'A',
                    'rgb'        => '#FE9DAF',
                    'short-name' => 'anothereruption',
                ],
                [
                    'color'      => '#008100',
                    'letter'     => 'B',
                    'rgb'        => '#008100',
                    'short-name' => 'brokengears',
                ],
                [
                    'color'      => '#FF7109',
                    'letter'     => 'C',
                    'rgb'        => '#FF7109',
                    'short-name' => 'cheating',
                ],
            ],
            [
                'A' => [
                    'name'       => 'anothereruption',
                    'externalid' => 'anothereruption',
                    'timelimit'  => 10,
                    'color'      => '#FE9DAF',
                ],
                'B' => [
                    'name'       => 'brokengears',
                    'externalid' => 'brokengears',
                    'timelimit'  => 10,
                    'color'      => '#008100',
                ],
                'C' => [
                    'name'       => 'cheating',
                    'externalid' => 'cheating',
                    'timelimit'  => 10,
                    'color'      => '#FF7109',
                ],
            ],
        ];
        yield [
            [
                [
                    'ordinal'    => 0,
                    'id'         => 'accesspoints',
                    'label'      => 'A',
                    'time_limit' => 2,
                    'name'       => 'Access Points',
                    'rgb'        => '#FF0000',
                    'color'      => 'red'
                ],
                [
                    'ordinal'    => 1,
                    'id'         => 'brexitnegotiations',
                    'label'      => 'B',
                    'time_limit' => 6,
                    'name'       => 'Brexit Negotiations',
                    'rgb'        => '#0422D8',
                    'color'      => 'mediumblue'
                ],
                [
                    'ordinal'    => 2,
                    'id'         => 'circuitdesign',
                    'label'      => 'C',
                    'time_limit' => 6,
                    'name'       => 'Circuit Board Design',
                    'rgb'        => '#008100',
                    'color'      => 'green'
                ],
            ],
            [
                'A' => [
                    'name'       => 'Access Points',
                    'externalid' => 'accesspoints',
                    'timelimit'  => 2,
                    'color'      => '#FF0000',
                ],
                'B' => [
                    'name'       => 'Brexit Negotiations',
                    'externalid' => 'brexitnegotiations',
                    'timelimit'  => 6,
                    'color'      => '#0422D8',
                ],
                'C' => [
                    'name'       => 'Circuit Board Design',
                    'externalid' => 'circuitdesign',
                    'timelimit'  => 6,
                    'color'      => '#008100',
                ],
            ],
        ];
    }

    public function testImportAccountsTsvSuccess(): void
    {
        $this->loadFixtures([TeamWithExternalIdEqualsOneFixture::class, TeamWithExternalIdEqualsTwoFixture::class]);

        // We test all account types twice:
        // - Team without postfix
        // - Team with postfix
        // - Judge
        // - Admin
        // - Analyst (will be ignored)
        // We also set the IP address for some accounts.
        $accounts = <<<EOF
accounts	1
team	Team 1	team001	password1
team	Team 2	team2	password2	1.2.3.4
team	Team 2 user a	team02a	password3	5.6.7.8
team	Team 2 user b	team02b	password4
judge	Judge member 1	judge1	password5
judge	Another judge member	judge2	password6	9.10.11.12
jury	Wrongly named judge member	judge3	password6	9.10.11.12
admin	Some admin	adminx	password7
admin	Another admin	adminy	password8
analyst	Analyst number 1	analyst1	password9	13.14.15.16
analyst	Analyst two	analyst2	password10
balloon	Balloon station	balloon1	password11
balloon	Backup balloon station	balloon2	password12
EOF;

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'accounts-tsv');
        file_put_contents($fileName, $accounts);
        $file = new UploadedFile($fileName, 'accounts.tsv');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importTsv('accounts', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        $this->testImportAccounts($importCount, $message, true);
    }

    public function testImportAccountsJsonSuccess(): void
    {
        // We test all account types twice:
        // - Team
        // - Judge
        // - Admin
        // - Analyst (will be ignored)
        // We also set the IP address for some accounts.
        $accounts = <<<EOF
- id: team001
  username: team001
  name: Team 1
  password: password1
  type: team
  team_id: domjudge
- id: team2
  username: team2
  name: Team 2
  password: password2
  type: team
  team_id: exteam
  ip: 1.2.3.4
- id: judge1
  username: judge1
  name: Judge member 1
  password: password5
  type: judge
- id: judge2
  username: judge2
  name: Another judge member
  password: password6
  type: judge
  ip: 9.10.11.12
- id: judge3
  username: judge3
  name: Wrongly named judge member
  password: password6
  type: jury
  ip: 9.10.11.12
- id: adminx
  username: adminx
  name: Some admin
  password: password7
  type: admin
- id: adminy
  username: adminy
  name: Another admin
  password: password8
  type: admin
- id: analyst1
  username: analyst1
  name: Analyst number 1
  password: password9
  type: analyst
  ip: 13.14.15.16
- id: analyst2
  username: analyst2
  name: Analyst two
  password: password10
  type: analyst
- id: balloon1
  username: balloon1
  name: Balloon station
  password: password11
  type: balloon
- id: balloon2
  username: balloon2
  name: Backup balloon station
  password: password12
  type: balloon
EOF;

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'accounts-yaml');
        file_put_contents($fileName, $accounts);
        $file = new UploadedFile($fileName, 'accounts.yaml');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('accounts', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        $this->testImportAccounts($importCount, $message, false);
    }

    public function testImportAccountsJsonError(): void
    {
        $accounts = <<<EOF
- id: team001
  username: team2//
  name: Team 1
  password: password1
  type: team
  team_id: 1
- id: team2
  username: team2
  name: Team 2
  password: password2
  type: team
  team_id: 2
  ip: 1.2.3.4
EOF;

        $em = static::getContainer()->get(EntityManagerInterface::class);
        $preCount = $em->getRepository(User::class)->count([]);

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'accounts-yaml');
        file_put_contents($fileName, $accounts);
        $file = new UploadedFile($fileName, 'accounts.yaml');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('accounts', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertEquals(-1, $importCount);
        self::assertMatchesRegularExpression('/Only alphanumeric characters and _-@. are allowed/', $message);

        $postCount = $em->getRepository(User::class)->count([]);
        self::assertEquals($preCount, $postCount);
    }

    protected function testImportAccounts(int $importCount, ?string $message, bool $forTsv): void
    {
        $expectedUsers = [
            [
                'roles' => ['team'],
                'name' => 'Team 1',
                'username' => 'team001',
                'password' => 'password1',
                'team' => [
                    'id' => 1,
                ],
            ],
            [
                'roles' => ['team'],
                'name' => 'Team 2',
                'username' => 'team2',
                'password' => 'password2',
                'ip' => '1.2.3.4',
                'team' => [
                    'id' => 2,
                ],
            ],
            [
                'roles' => ['jury', 'team'],
                'name' => 'Judge member 1',
                'username' => 'judge1',
                'password' => 'password5',
                'team' => [
                    'name' => 'Judge member 1',
                    'category' => 'Jury',
                    'members' => 'Judge member 1',
                ],
            ],
            [
                'roles' => ['jury', 'team'],
                'name' => 'Another judge member',
                'username' => 'judge2',
                'password' => 'password6',
                'ip' => '9.10.11.12',
                'team' => [
                    'name' => 'Another judge member',
                    'category' => 'Jury',
                    'members' => 'Another judge member',
                ],
            ],
            [
                'roles' => ['jury', 'team'],
                'name' => 'Wrongly named judge member',
                'username' => 'judge3',
                'password' => 'password6',
                'ip' => '9.10.11.12',
                'team' => [
                    'name' => 'Wrongly named judge member',
                    'category' => 'Jury',
                    'members' => 'Wrongly named judge member',
                ],
            ],
            [
                'roles' => ['admin','team'],
                'name' => 'Some admin',
                'username' => 'adminx',
                'password' => 'password7',
                'team' => [
                    'name' => 'Some admin',
                    'category' => 'Jury',
                    'members' => 'Some admin',
                ],
            ],
            [
                'roles' => ['admin','team'],
                'name' => 'Another admin',
                'username' => 'adminy',
                'password' => 'password8',
                'team' => [
                    'name' => 'Another admin',
                    'category' => 'Jury',
                    'members' => 'Another admin',
                ],
            ],
            [
                'roles' => ['balloon'],
                'name' => 'Balloon station',
                'username' => 'balloon1',
                'password' => 'password11',
            ],
            [
                'roles' => ['balloon'],
                'name' => 'Backup balloon station',
                'username' => 'balloon2',
                'password' => 'password12',
            ],
        ];
        if ($forTsv) {
            $expectedUsers = [...$expectedUsers, [
                'roles' => ['team'],
                'name' => 'Team 2 user a',
                'username' => 'team02a',
                'password' => 'password3',
                'ip' => '5.6.7.8',
                'team' => [
                    'id' => 2,
                ],
            ], [
                'roles' => ['team'],
                'name' => 'Team 2 user b',
                'username' => 'team02b',
                'password' => 'password4',
                'team' => [
                    'id' => 2,
                ],
            ]];
        }
        $unexpectedUsers = ['analyst1', 'analyst2'];

        self::assertEquals(count($expectedUsers), $importCount);
        self::assertNull($message);

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);

        $userPasswordHasher = static::getContainer()->get(UserPasswordHasherInterface::class);

        // Check for all expected users.
        foreach ($expectedUsers as $data) {
            $user = $em->getRepository(User::class)->findOneBy(['username' => $data['username']]);
            self::assertNotNull($user, "User $data[username] does not exist");
            self::assertEquals($data['name'], $user->getName());
            self::assertTrue($userPasswordHasher->isPasswordValid($user, $data['password']));
            // To verify roles we need to sort them.
            $roles = $user->getRoleList();
            sort($roles);
            $dataRoles = $data['roles'];
            sort($dataRoles);
            self::assertEquals($dataRoles, $roles);

            // Verify the team.
            if (isset($data['team'])) {
                self::assertNotNull($user->getTeam(), $data['username']);
                $team = $user->getTeam();
                if (isset($data['team']['id'])) {
                    self::assertEquals($data['team']['id'], $team->getTeamid());
                }
                if (isset($data['team']['name'])) {
                    self::assertEquals($data['team']['name'], $team->getName());
                }
                if (isset($data['team']['category'])) {
                    self::assertNotFalse($team->getCategories()->first());
                    self::assertEquals($data['team']['category'], $team->getCategories()->first()->getName());
                }
                if (isset($data['team']['members'])) {
                    self::assertEquals($data['team']['members'], $team->getPublicDescription());
                }
            } else {
                self::assertNull($user->getTeam());
            }
        }

        // Check all unexpected users are not present.
        foreach ($unexpectedUsers as $username) {
            $user = $em->getRepository(User::class)->findOneBy(['username' => $username]);
            self::assertNull($user, "User $username should not exist");
        }
    }

    public function testImportTeamsTsv(): void
    {
        // Example from the manual, but we have changed the ID's to not mix them with fixtures
        $teamsData = <<<EOF
File_Version	2
11	447047	24	¡i¡i¡	Lund University	LU	SWE	INST-42
12	447837	25	Pleading not FAUlty	Friedrich-Alexander-University Erlangen-Nuremberg	FAU	DEU	INST-43
13	447057	24	Another team from Lund	Lund University	LU	SWE	INST-42
EOF;

        $expectedTeams = [
            [
                'externalid' => '11',
                'icpcid' => '447047',
                'label' => null,
                'name' => '¡i¡i¡',
                'category' => [
                    'externalid' => '24',
                ],
                'affiliation' => [
                    'externalid' => '42',
                    'shortname' => 'LU',
                    'name' => 'Lund University',
                    'country' => 'SWE',
                ],
            ], [
                'externalid' => '12',
                'icpcid' => '447837',
                'label' => null,
                'name' => 'Pleading not FAUlty',
                'category' => [
                    'externalid' => '25',
                ],
                'affiliation' => [
                    'externalid' => '43',
                    'shortname' => 'FAU',
                    'name' => 'Friedrich-Alexander-University Erlangen-Nuremberg',
                    'country' => 'DEU',
                ],
            ], [
                'externalid' => '13',
                'icpcid' => '447057',
                'label' => null,
                'name' => 'Another team from Lund',
                'category' => [
                    'externalid' => '24',
                ],
                'affiliation' => [
                    'externalid' => '42',
                    'shortname' => 'LU',
                    'name' => 'Lund University',
                    'country' => 'SWE',
                ],
            ],
        ];

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'teams-tsv');
        file_put_contents($fileName, $teamsData);
        $file = new UploadedFile($fileName, 'teams.tsv');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importTsv('teams', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertNull($message);
        self::assertEquals(count($expectedTeams), $importCount);

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);

        foreach ($expectedTeams as $data) {
            $team = $em->getRepository(Team::class)->findOneBy(['externalid' => $data['externalid']]);
            self::assertNotNull($team, "Team $data[name] does not exist");
            self::assertEquals($data['icpcid'], $team->getIcpcId());
            self::assertEquals($data['label'], $team->getLabel());
            self::assertEquals($data['name'], $team->getName());
            self::assertNull($team->getLocation());
            self::assertEquals($data['category']['externalid'], $team->getCategories()->first()->getExternalid());
            self::assertEquals($data['affiliation']['externalid'], $team->getAffiliation()->getExternalid());
            self::assertEquals($data['affiliation']['shortname'], $team->getAffiliation()->getShortname());
            self::assertEquals($data['affiliation']['name'], $team->getAffiliation()->getName());
            self::assertEquals($data['affiliation']['country'], $team->getAffiliation()->getCountry());
        }
    }

    public function testImportTeamsJson(): void
    {
        $this->loadFixture(NonSortOrderTeamCategoryFixture::class);

        // Example from the manual, but we have changed the ID's to not mix them with fixtures and
        // we explicitly use a different label for the first team and no label for the second
        // Also we explicitly test for the label '0', since that is a special case
        $teamsData = <<<EOF
[{
    "id": "11",
    "icpc_id": "447047",
    "label": "team1",
    "group_ids": ["24"],
    "name": "¡i¡i¡",
    "organization_id": "INST-42",
    "location": {"description": "AUD 10"}
}, {
    "id": "12",
    "icpc_id": "447837",
    "group_ids": ["25"],
    "name": "Pleading not FAUlty",
    "organization_id": "INST-43"
}, {
    "id": "13",
    "icpc_id": "123456",
    "label": "0",
    "group_ids": ["24", "colorcat"],
    "name": "Team with label 0",
    "organization_id": "INST-44"
}]
EOF;

        $expectedTeams = [
            [
                'externalid' => '11',
                'icpcid' => '447047',
                'label' => 'team1',
                'name' => '¡i¡i¡',
                'location' => 'AUD 10',
                'categories' => ['24'],
                'affiliation' => 'INST-42',
            ], [
                'externalid' => '12',
                'icpcid' => '447837',
                'label' => null,
                'name' => 'Pleading not FAUlty',
                'location' => null,
                'categories' => ['25'],
                'affiliation' => 'INST-43',
            ], [
                'externalid' => '13',
                'icpcid' => '123456',
                'label' => '0',
                'name' => 'Team with label 0',
                'location' => null,
                'categories' => ['24', 'colorcat'],
                'affiliation' => 'INST-44',
            ],
        ];

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'teams-json');
        file_put_contents($fileName, $teamsData);
        $file = new UploadedFile($fileName, 'teams.json');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('teams', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertNull($message);
        self::assertEquals(count($expectedTeams), $importCount);

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);

        foreach ($expectedTeams as $data) {
            $team = $em->getRepository(Team::class)->findOneBy(['externalid' => $data['externalid']]);
            self::assertNotNull($team, "Team $data[name] does not exist");
            self::assertEquals($data['icpcid'], $team->getIcpcId());
            self::assertEquals($data['label'], $team->getLabel());
            self::assertEquals($data['location'], $team->getLocation());
            self::assertEquals($data['name'], $team->getName());
            $categoryIds = $team->getCategories()->map(fn(TeamCategory $category) => $category->getExternalid())->toArray();
            self::assertEqualsCanonicalizing($data['categories'], $categoryIds);
            self::assertEquals($data['affiliation'], $team->getAffiliation()->getExternalid());
        }
    }

    public function testImportTeamsJsonError(): void
    {
        $teamsData = <<<EOF
[{
    "id": "11",
    "icpc_id": "447047",
    "label": "team1",
    "group_ids": ["24"],
    "organization_id": "INST-42",
    "location": {"description": "AUD 10"}
}, {
    "id": "12",
    "icpc_id": "447837",
    "group_ids": ["25"],
    "name": "Pleading not FAUlty",
    "organization_id": "INST-43"
}, {
    "id": "14",
    "icpc_id": "112233",
    "group_ids": [],
    "name": "Team with empty groups",
    "organization_id": "INST-45"
}, {
    "id": "15",
    "icpc_id": "445566",
    "name": "Team with no groups",
    "organization_id": "INST-46"
}]
EOF;
        $em = static::getContainer()->get(EntityManagerInterface::class);
        $preCount = $em->getRepository(Team::class)->count([]);

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'teams-json');
        file_put_contents($fileName, $teamsData);
        $file = new UploadedFile($fileName, 'teams.json');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('teams', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertMatchesRegularExpression('/.*`name`: This value should not be blank.*/', $message);
        self::assertMatchesRegularExpression('/.*`categories`: Team Team with empty groups must be in exactly one scoring category.*/', $message);
        self::assertMatchesRegularExpression('/.*`categories`: Team Team with no groups must be in exactly one scoring category.*/', $message);
        self::assertEquals(-1, $importCount);

        $postCount = $em->getRepository(Team::class)->count([]);
        self::assertEquals($preCount, $postCount);
    }

    public function testImportTeamsJsonErrorEmptyString(): void
    {
        $teamsData = <<<EOF
[{
    "id": "11",
    "icpc_id": "447047",
    "label": "team1",
    "name": "",
    "group_ids": ["24"],
    "organization_id": "INST-42",
    "location": {"description": "AUD 10"}
}, {
    "id": "12",
    "icpc_id": "447837",
    "group_ids": ["25"],
    "name": "Pleading not FAUlty",
    "organization_id": "INST-43"
}]
EOF;
        $em = static::getContainer()->get(EntityManagerInterface::class);
        $preCount = $em->getRepository(Team::class)->count([]);

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'teams-json');
        file_put_contents($fileName, $teamsData);
        $file = new UploadedFile($fileName, 'teams.json');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('teams', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertMatchesRegularExpression('/.*`name`: This value should not be blank.*/', $message);
        self::assertEquals(-1, $importCount);

        $postCount = $em->getRepository(Team::class)->count([]);
        self::assertEquals($preCount, $postCount);
    }

    public function testImportGroupsTsv(): void
    {
        // Example from the manual
        $groupsData = <<<EOF
File_Version	1
13337	Companies
47	Participants
23	Spectators
EOF;

        $expectedGroups = [
            [
                'externalid' => '13337',
                'name' => 'Companies',
                'visible' => true,
            ], [
                'externalid' => '47',
                'name' => 'Participants',
                'visible' => true,
            ], [
                'externalid' => '23',
                'name' => 'Spectators',
                'visible' => true,
            ],
        ];

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'groups-tsv');
        file_put_contents($fileName, $groupsData);
        $file = new UploadedFile($fileName, 'groups.tsv');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importTsv('groups', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertNull($message);
        self::assertEquals(count($expectedGroups), $importCount);

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);

        foreach ($expectedGroups as $data) {
            $category = $em->getRepository(TeamCategory::class)->findOneBy(['externalid' => $data['externalid']]);
            self::assertNotNull($category, "Team category $data[name] does not exist");
            self::assertEquals($data['name'], $category->getName());
            self::assertEquals($data['visible'], $category->getVisible());
        }
    }

    public function testImportGroupsJson(): void
    {
        // Example from the manual
        $groupsData = <<<EOF
[{
    "id": "13337",
    "icpc_id": "123",
    "name": "Companies",
    "hidden": true
}, {
    "id": "47",
    "name": "Participants"
}, {
    "id": "23",
    "name": "Spectators"
}, {
    "id": "24",
    "name": "Color",
    "types": ["background"],
    "color": "#123123"
}, {
    "id": "25",
    "name": "CSS",
    "types": ["css-class"],
    "css_class": "test"
}]
EOF;

        $expectedGroups = [
            [
                'externalid' => '13337',
                'name' => 'Companies',
                'icpcid' => '123',
                'sortorder' => 0,
                'visible' => false,
            ], [
                'externalid' => '47',
                'name' => 'Participants',
                'sortorder' => 0,
                'visible' => true,
            ], [
                'externalid' => '23',
                'name' => 'Spectators',
                'sortorder' => 0,
                'visible' => true,
            ], [
                'externalid' => '24',
                'name' => 'Color',
                'types' => [TeamCategory::TYPE_BACKGROUND],
                'sortorder' => null,
                'visible' => true,
                'color' => '#123123',
            ], [
                'externalid' => '25',
                'name' => 'CSS',
                'types' => [TeamCategory::TYPE_CSS_CLASS],
                'sortorder' => null,
                'visible' => true,
                'cssClass' => 'test',
            ],
        ];

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'groups-json');
        file_put_contents($fileName, $groupsData);
        $file = new UploadedFile($fileName, 'groups.json');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('groups', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertNull($message);
        self::assertEquals(count($expectedGroups), $importCount);

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);

        foreach ($expectedGroups as $data) {
            $category = $em->getRepository(TeamCategory::class)->findOneBy(['externalid' => $data['externalid']]);
            self::assertNotNull($category, "Team cagegory $data[name] does not exist");
            self::assertEquals($data['icpcid'] ?? null, $category->getIcpcId());
            self::assertEquals($data['name'], $category->getName());
            self::assertEquals($data['types'] ?? [TeamCategory::TYPE_SCORING, TeamCategory::TYPE_BADGE_TOP], $category->getTypes());
            self::assertEquals($data['visible'], $category->getVisible());
            self::assertEquals($data['sortorder'] ?? null, $category->getSortorder());
            self::assertEquals($data['color'] ?? null, $category->getColor());
            self::assertEquals($data['cssClass'] ?? null, $category->getCssClass());
        }
    }

    public function testImportGroupsJsonError(): void
    {
        // Example from the manual
        $groupsData = <<<EOF
[{
    "id": "13337",
    "icpc_id": "123",
    "hidden": true
}, {
    "id": "47",
    "name": "Participants"
}]
EOF;

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);
        $preCount = $em->getRepository(TeamCategory::class)->count([]);

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'groups-json');
        file_put_contents($fileName, $groupsData);
        $file = new UploadedFile($fileName, 'groups.json');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('groups', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertMatchesRegularExpression('/.*`name`: This value should not be blank.*/', $message);
        self::assertEquals(-1, $importCount);

        $postCount = $em->getRepository(TeamCategory::class)->count([]);
        self::assertEquals($preCount, $postCount);
    }


    public function testImportOrganizationsJson(): void
    {
        // Example from the manual
        $organizationsData = <<<EOF
[{
    "id": "INST-42",
    "icpc_id": "42",
    "name": "LU",
    "formal_name": "Lund University",
    "country": "SWE"
}, {
    "id": "INST-43",
    "icpc_id": "43",
    "name": "FAU",
    "formal_name": "Friedrich-Alexander-University Erlangen-Nuremberg",
    "country": "DEU"
}]
EOF;

        $expectedOrganizations = [
            [
                'externalid' => 'INST-42',
                'icpcid' => '42',
                'shortname' => 'LU',
                'name' => 'Lund University',
                'country' => 'SWE',
            ], [
                'externalid' => 'INST-43',
                'icpcid' => '43',
                'shortname' => 'FAU',
                'name' => 'Friedrich-Alexander-University Erlangen-Nuremberg',
                'country' => 'DEU',
            ],
        ];

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'organizations-json');
        file_put_contents($fileName, $organizationsData);
        $file = new UploadedFile($fileName, 'organizations.json');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('organizations', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertNull($message);
        self::assertEquals(count($expectedOrganizations), $importCount);

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);

        foreach ($expectedOrganizations as $data) {
            $affiliation = $em->getRepository(TeamAffiliation::class)->findOneBy(['externalid' => $data['externalid']]);
            self::assertNotNull($affiliation, "Team affiliation $data[name] does not exist");
            self::assertEquals($data['icpcid'], $affiliation->getIcpcId());
            self::assertEquals($data['shortname'], $affiliation->getShortname());
            self::assertEquals($data['name'], $affiliation->getName());
            self::assertEquals($data['country'], $affiliation->getCountry());
        }
    }

    public function testImportOrganizationsErrorJson(): void
    {
        // Example from the manual
        $organizationsData = <<<EOF
[{
    "id": "INST-42",
    "icpc_id": "42",
    "name": "LU",
    "formal_name": "Lund University",
    "country": "XXX"
}, {
    "id": "INST-43",
    "icpc_id": "43",
    "name": "FAU",
    "formal_name": "Friedrich-Alexander-University Erlangen-Nuremberg",
    "country": "DEU"
}]
EOF;

        $em = static::getContainer()->get(EntityManagerInterface::class);
        $preCount = $em->getRepository(TeamAffiliation::class)->count([]);

        $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'organizations-json');
        file_put_contents($fileName, $organizationsData);
        $file = new UploadedFile($fileName, 'organizations.json');
        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);
        $importCount = $importExportService->importJson('organizations', $file, $message);
        // Remove the file, we don't need it anymore.
        unlink($fileName);

        self::assertMatchesRegularExpression('/ISO3166-1 alpha-3 values are allowed/', $message);
        self::assertEquals(-1, $importCount);

        $postCount = $em->getRepository(TeamAffiliation::class)->count([]);
        self::assertEquals($preCount, $postCount);
    }


    protected function getContest(int|string $cid): Contest
    {
        // First clear the entity manager to have all data.
        static::getContainer()->get(EntityManagerInterface::class)->clear();
        return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Contest::class)->findOneBy(['externalid' => $cid]);
    }

    #[DataProvider('provideGetResultsData')]
    public function testGetResultsData(bool $full, bool $honors, string $dataSet, string $expectedResultsFile): void
    {
        // Set up some results we can test with
        // This data is based on the ICPC World Finals 47
        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get(EntityManagerInterface::class);

        $startTime = new DateTimeImmutable('2023-05-01 08:00:00');

        $medalData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/' . $dataSet . '/sample-medals.json'), true);

        $contest = (new Contest())
            ->setName('ICPC World Finals 47')
            ->setShortname('wf47')
            ->setStarttimeString($startTime->format(DateTimeInterface::ATOM))
            ->setEndtimeString($startTime->add(new DateInterval('PT5H'))->format(DateTimeInterface::ATOM))
            ->setMedalsEnabled(true)
            ->setGoldMedals($medalData['medals']['gold'])
            ->SetSilverMedals($medalData['medals']['silver'])
            ->setBronzeMedals($medalData['medals']['bronze']);

        $groupsById = [];
        $groupsData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/' . $dataSet . '/sample-groups.json'), true);
        foreach ($groupsData as $groupData) {
            $group = (new TeamCategory())
                ->setExternalid($groupData['id'])
                ->setName($groupData['name'])
                ->setSortorder(37);
            $em->persist($group);
            $em->flush();
            $groupsById[$group->getExternalid()] = $group;
            if (in_array($group->getExternalid(), $medalData['medal_categories'], true)) {
                $contest->addMedalCategory($group);
            }
        }

        $em->persist($contest);
        $em->flush();

        $teamsData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/'. $dataSet . '/sample-teams.json'), true);
        /** @var array<string,Team> $teamsById */
        $teamsById = [];
        /** @var array<string,Team> $teamsByIcpcId */
        $teamsByIcpcId = [];
        foreach ($teamsData as $teamData) {
            $team = (new Team())
                ->setExternalid($teamData['id'])
                ->setIcpcid($teamData['icpc_id'])
                ->setName($teamData['name'])
                ->setDisplayName($teamData['display_name'])
                ->addCategory($groupsById[$teamData['group_ids'][0]]);
            $em->persist($team);
            $em->flush();
            $teamsById[$team->getExternalid()] = $team;
            $teamsByIcpcId[$team->getIcpcId()] = $team;
        }

        $problemsData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/'. $dataSet . '/sample-problems.json'), true);
        $contestProblemsById = [];
        foreach ($problemsData as $problemData) {
            $problem = (new Problem())
                ->setExternalid($problemData['id'])
                ->setName($problemData['name']);
            $contestProblem = (new ContestProblem())
                ->setProblem($problem)
                ->setContest($contest)
                ->setColor($problemData['rgb'])
                ->setShortname($problemData['label']);
            $em->persist($problem);
            $em->persist($contestProblem);
            $contest->addProblem($contestProblem);
            $em->flush();
            $contestProblemsById[$contestProblem->getExternalid()] = $contestProblem;
        }

        $cpp = $em->getRepository(Language::class)->findByExternalId('cpp');

        // We use direct queries here to speed this up
        $submissionInsertQuery = $em->getConnection()->prepare('INSERT INTO submission (teamid, cid, probid, langid, submittime) VALUES (:teamid, :cid, :probid, :langid, :submittime)');
        $judgingInsertQuery = $em->getConnection()->prepare('INSERT INTO judging (uuid, submitid, result) VALUES (:uuid, :submitid, :result)');

        $submissionInsertQuery->bindValue('cid', $contest->getCid());
        $submissionInsertQuery->bindValue('langid', $cpp->getLangid());

        $scoreboardData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/'. $dataSet . '/sample-scoreboard.json'), true);
        foreach ($scoreboardData['rows'] as $scoreboardRow) {
            $team = $teamsById[$scoreboardRow['team_id']];
            $submissionInsertQuery->bindValue('teamid', $team->getTeamid());
            foreach ($scoreboardRow['problems'] as $problemData) {
                if ($problemData['solved']) {
                    $contestProblem = $contestProblemsById[$problemData['problem_id']];
                    // Add fake submission for this problem. First add wrong ones
                    for ($i = 0; $i < $problemData['num_judged'] - 1; $i++) {
                        $submissionInsertQuery->bindValue('probid', $contestProblem->getProbid());
                        $submissionInsertQuery->bindValue('submittime', $startTime
                            ->add(new DateInterval('PT' . $problemData['time'] . 'M'))
                            ->sub(new DateInterval('PT1M'))
                            ->getTimestamp());
                        $submissionInsertQuery->executeQuery();
                        $submitId = $em->getConnection()->lastInsertId();
                        $judgingInsertQuery->bindValue('uuid', Uuid::uuid4()->toString());
                        $judgingInsertQuery->bindValue('submitid', $submitId);
                        $judgingInsertQuery->bindValue('result', 'wrong-awnser');
                        $judgingInsertQuery->executeQuery();
                    }
                    // Add correct submission
                    $submissionInsertQuery->bindValue('probid', $contestProblem->getProbid());
                    $submissionInsertQuery->bindValue('submittime', $startTime
                        ->add(new DateInterval('PT' . $problemData['time'] . 'M'))
                        ->getTimestamp());
                    $submissionInsertQuery->executeQuery();
                    $submitId = $em->getConnection()->lastInsertId();
                    $judgingInsertQuery->bindValue('uuid', Uuid::uuid4()->toString());
                    $judgingInsertQuery->bindValue('submitid', $submitId);
                    $judgingInsertQuery->bindValue('result', 'correct');
                    $judgingInsertQuery->executeQuery();
                }
            }
        }

        /** @var ScoreboardService $scoreboardService */
        $scoreboardService = static::getContainer()->get(ScoreboardService::class);
        $scoreboardService->refreshCache($contest);

        /** @var ImportExportService $importExportService */
        $importExportService = static::getContainer()->get(ImportExportService::class);

        /** @var RequestStack $requestStack */
        $requestStack = static::getContainer()->get(RequestStack::class);
        $request = new Request();
        $request->cookies->set('domjudge_cid', (string)$contest->getExternalid());
        $requestStack->push($request);

        $results = $importExportService->getResultsData(37, $full, $honors);

        $resultsContents = file_get_contents(__DIR__ . '/../Fixtures/' . $dataSet . '/' . $expectedResultsFile);
        $resultsContents = substr($resultsContents, strpos($resultsContents, "\n") + 1);
        // Prefix file with a fake header, so we can deserialize them
        $resultsContents = "team_id\trank\taward\tnum_solved\ttotal_time\ttime_of_last_submission\tgroup_winner\n" . $resultsContents;

        $serializer = static::getContainer()->get(SerializerInterface::class);

        $expectedResults = $serializer->deserialize($resultsContents, ResultRow::class . '[]', 'csv', [
            CsvEncoder::DELIMITER_KEY => "\t",
        ]);

        self::assertEquals($expectedResults, $results);
    }

    public static function provideGetResultsData(): Generator
    {
        yield [true, true, 'wf', 'results-full-honors.tsv'];
        yield [false, true, 'wf', 'results-wf-honors.tsv'];
        yield [true, false, 'wf', 'results-full-ranked.tsv'];
        yield [false, false, 'wf', 'results-wf-ranked.tsv'];
        yield [true, true, 'sample', 'results-full-honors.tsv'];
        yield [false, true, 'sample', 'results-wf-honors.tsv'];
        yield [true, false, 'sample', 'results-full-ranked.tsv'];
        yield [false, true, 'sample', 'results-wf-honors.tsv'];
    }
}
