path = tempnam(sys_get_temp_dir(), 'geo-csv-'); } protected function tearDown(): void { unlink($this->path); } private function inspect(string $rows): array { file_put_contents($this->path, implode(';', TerritorialCsvAudit::HEADERS)."\n".$rows); return (new TerritorialCsvAudit)->inspect($this->path, ['01']); } public function test_preserves_leading_zero_codes_and_counts_identical_duplicates(): void { $row = '01;Synthetic;0101;District;010101;Post;01010101;Locality;village;-12.5;35.1'; $report = $this->inspect($row."\n".$row); $this->assertSame('structurally_valid_requires_review', $report['status']); $this->assertSame(1, $report['duplicate_records']); $this->assertSame(1, $report['candidate_counts_by_province']['01']['locality']); $this->assertSame(hash_file('sha256', $this->path), $report['sha256']); } public function test_conflicting_names_and_coordinates_cancel_whole_dataset(): void { $report = $this->inspect("01;Synthetic;0101;First;010101;Post;01010101;Locality;village;0;0\n01;Synthetic;0101;Other;010101;Post;01010101;Locality;village;1;0"); $this->assertSame('rejected', $report['status']); $this->assertContains('conflicting_code:district', $report['errors'][0]['issues']); $this->assertContains('conflicting_code:locality', $report['errors'][0]['issues']); } public function test_no_inferred_parent_or_coordinates_and_unknown_province_are_rejected(): void { $report = $this->inspect('02;Synthetic;0201;District;;;02010101;Locality;village;91;'); $this->assertSame('rejected', $report['status']); foreach (['unknown_province', 'locality_requires_reviewed_parent_exception', 'incomplete_coordinates', 'invalid_coordinate:latitude'] as $issue) { $this->assertContains($issue, $report['errors'][0]['issues']); } } public function test_district_only_rows_do_not_create_localities(): void { $report = $this->inspect('01;Synthetic;0101;District;;;;;;;'); $this->assertSame('structurally_valid_requires_review', $report['status']); $this->assertArrayNotHasKey('locality', $report['candidate_counts_by_province']['01']); } public function test_empty_csv_is_rejected(): void { $this->assertSame('rejected', $this->inspect('')['status']); } public function test_wrong_delimiter_is_not_silently_interpreted(): void { file_put_contents($this->path, implode(',', TerritorialCsvAudit::HEADERS)); $this->expectException(RuntimeException::class); (new TerritorialCsvAudit)->inspect($this->path, ['01']); } }