import tempfile
import unittest
import urllib.error
from pathlib import Path
from unittest import mock

import dspace_wgetter


def make_oai_root(records_xml: str):
    payload = f'''
<oai:OAI-PMH
    xmlns:oai='http://www.openarchives.org/OAI/2.0/'
    xmlns:mets='http://www.loc.gov/METS/'
    xmlns:xlink='http://www.w3.org/1999/xlink'>
  <oai:ListRecords>
    {records_xml}
  </oai:ListRecords>
</oai:OAI-PMH>
'''.strip()
    return dspace_wgetter.parse_oai_xml(payload.encode('utf-8'), 'test-oai.xml')


def make_record(identifier: str, href: str, *, size: int = 123, set_spec: str = 'col_1234_99') -> str:
    return f'''
<oai:record xmlns:oai='http://www.openarchives.org/OAI/2.0/' xmlns:mets='http://www.loc.gov/METS/'
    xmlns:xlink='http://www.w3.org/1999/xlink'>
  <oai:header>
    <oai:identifier>{identifier}</oai:identifier>
    <oai:setSpec>{set_spec}</oai:setSpec>
  </oai:header>
  <oai:metadata>
    <mets:mets>
      <mets:fileSec>
        <mets:fileGrp USE='ORIGINAL'>
          <mets:file SIZE='{size}'>
            <mets:FLocat xlink:href='{href}' />
          </mets:file>
        </mets:fileGrp>
      </mets:fileSec>
    </mets:mets>
  </oai:metadata>
</oai:record>
'''.strip()


class ManifestTests(unittest.TestCase):
    def test_write_and_read_download_rows_roundtrip(self):
        rows = [
            ('1895/file1.pdf', 'https://repo.example.org/file1.pdf', 123),
            ('1896/file2.pdf', 'https://repo.example.org/file2.pdf', None),
        ]

        with tempfile.TemporaryDirectory() as tmpdir:
            path = Path(tmpdir) / 'failed_downloads.tsv'
            dspace_wgetter.write_download_rows(path, rows)

            self.assertEqual(rows, dspace_wgetter.read_download_rows(path))

    def test_remaining_resume_rows_keeps_failed_then_unprocessed(self):
        failed_rows = [('1895/file2.pdf', 'https://repo.example.org/file2.pdf', 2)]
        all_rows = [
            ('1895/file1.pdf', 'https://repo.example.org/file1.pdf', 1),
            ('1895/file2.pdf', 'https://repo.example.org/file2.pdf', 2),
            ('1895/file3.pdf', 'https://repo.example.org/file3.pdf', 3),
        ]

        self.assertEqual(
            [
                ('1895/file2.pdf', 'https://repo.example.org/file2.pdf', 2),
                ('1895/file3.pdf', 'https://repo.example.org/file3.pdf', 3),
            ],
            dspace_wgetter.remaining_resume_rows(failed_rows, all_rows, processed_count=2),
        )


class BitstreamCandidateTests(unittest.TestCase):
    def test_prefers_collection_origin_for_same_host_bitstream_url(self):
        url = 'http://repo.example.org:8080/bitstream/1234/5/1/file.pdf'
        collection_base_url = 'https://repo.example.org'

        self.assertEqual(
            [
                'https://repo.example.org/bitstream/1234/5/1/file.pdf',
                'http://repo.example.org:8080/bitstream/1234/5/1/file.pdf',
            ],
            dspace_wgetter.build_bitstream_url_candidates(url, collection_base_url),
        )

    def test_relative_bitstream_url_resolves_once(self):
        url = '/bitstream/1234/5/1/file.pdf'
        collection_base_url = 'https://repo.example.org'

        self.assertEqual(
            ['https://repo.example.org/bitstream/1234/5/1/file.pdf'],
            dspace_wgetter.build_bitstream_url_candidates(url, collection_base_url),
        )

    def test_different_host_url_is_not_rewritten(self):
        url = 'https://cdn.example.org/bitstream/1234/5/1/file.pdf'
        collection_base_url = 'https://repo.example.org'

        self.assertEqual(
            ['https://cdn.example.org/bitstream/1234/5/1/file.pdf'],
            dspace_wgetter.build_bitstream_url_candidates(url, collection_base_url),
        )


class PathPlanningTests(unittest.TestCase):
    def test_direct_collection_downloads_do_not_repeat_collection_name(self):
        self.assertEqual(
            '',
            dspace_wgetter.extract_item_collection_dir(
                [],
                input_set_spec='col_1234_15338',
                input_set_name='Kvali',
                set_map={},
            ),
        )

        planned_rows, collision_count = dspace_wgetter.plan_download_rows(
            [('', 'https://repo.example.org/bitstream/1234/1/1/Kvali_1895_N1.pdf', 1)]
        )

        self.assertEqual(0, collision_count)
        self.assertEqual(
            [('1895/Kvali_1895_N1.pdf', 'https://repo.example.org/bitstream/1234/1/1/Kvali_1895_N1.pdf', 1)],
            planned_rows,
        )

    def test_community_downloads_keep_collection_folder(self):
        self.assertEqual(
            'Kvali',
            dspace_wgetter.extract_item_collection_dir(
                ['col_1234_15338'],
                input_set_spec='com_1234_1',
                input_set_name='Newspapers',
                set_map={'col_1234_15338': 'Kvali'},
            ),
        )

        planned_rows, collision_count = dspace_wgetter.plan_download_rows(
            [('Kvali', 'https://repo.example.org/bitstream/1234/1/1/Kvali_1895_N1.pdf', 1)]
        )

        self.assertEqual(0, collision_count)
        self.assertEqual(
            [('Kvali/1895/Kvali_1895_N1.pdf', 'https://repo.example.org/bitstream/1234/1/1/Kvali_1895_N1.pdf', 1)],
            planned_rows,
        )

    def test_duplicate_file_names_get_stable_suffixes(self):
        planned_rows, collision_count = dspace_wgetter.plan_download_rows(
            [
                ('', 'https://repo.example.org/bitstream/1234/555/1/Kvali_1895_N1.pdf', 1),
                ('', 'https://repo.example.org/bitstream/1234/777/1/Kvali_1895_N1.pdf', 2),
            ]
        )

        self.assertEqual(2, collision_count)
        self.assertEqual(
            [
                ('1895/Kvali_1895_N1__555_1.pdf', 'https://repo.example.org/bitstream/1234/555/1/Kvali_1895_N1.pdf', 1),
                ('1895/Kvali_1895_N1__777_1.pdf', 'https://repo.example.org/bitstream/1234/777/1/Kvali_1895_N1.pdf', 2),
            ],
            planned_rows,
        )


class DownloadTests(unittest.TestCase):
    def test_normalized_404_stops_before_slow_harvested_fallback(self):
        attempted_urls: list[str] = []
        destination = Path(tempfile.gettempdir()) / 'dspace_wgetter_test_download.bin'
        destination.unlink(missing_ok=True)

        def fake_urlopen(request, timeout):
            attempted_urls.append(request.full_url)
            raise urllib.error.HTTPError(request.full_url, 404, 'Not Found', hdrs=None, fp=None)

        with tempfile.TemporaryDirectory() as tmpdir:
            download_log = Path(tmpdir) / 'download.log'
            with mock.patch('dspace_wgetter.urllib.request.urlopen', side_effect=fake_urlopen):
                result, failure_detail = dspace_wgetter.download_file(
                    'http://repo.example.org:8080/bitstream/1234/5/1/file.pdf',
                    destination,
                    download_log,
                    collection_base_url='https://repo.example.org',
                )

        self.assertIsNone(result)
        self.assertEqual('HTTP 404', failure_detail)
        self.assertEqual(['https://repo.example.org/bitstream/1234/5/1/file.pdf'], attempted_urls)


class HarvestTests(unittest.TestCase):
    def test_harvest_filters_invalid_oai_items_before_queue_build(self):
        root = make_oai_root(
            '\n'.join(
                [
                    make_record(
                        'oai:repo.example.org:1234/1',
                        'https://repo.example.org/bitstream/1234/1/1/Kvali_1895_N1.pdf',
                    ),
                    make_record(
                        'oai:repo.example.org:1234/2',
                        'https://repo.example.org/bitstream/1234/2/1/Kvali_1895_N2.pdf',
                    ),
                ]
            )
        )

        def fake_handle_status(base_url: str, item_handle: str, fetch_log: Path) -> int | None:
            return {'1234/1': 200, '1234/2': 404}[item_handle]

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp_path = Path(tmpdir)
            with mock.patch.object(dspace_wgetter, 'iter_oai_pages', return_value=[(1, root)]):
                with mock.patch.object(dspace_wgetter, 'fetch_item_handle_status', side_effect=fake_handle_status):
                    planned_rows, item_count, invalid_item_count, nofile_count, page_count, collision_count = (
                        dspace_wgetter.harvest_download_rows(
                            'https://repo.example.org',
                            'https://repo.example.org/oai/request',
                            input_set_spec='col_1234_99',
                            input_set_name='Kvali',
                            set_map={'col_1234_99': 'Kvali'},
                            validate_item_handles=True,
                            request_delay=0.0,
                            fetch_log=tmp_path / 'fetch.log',
                            run_log=tmp_path / 'run.log',
                            download_urls_raw=tmp_path / 'download_urls.raw.tsv',
                            download_urls=tmp_path / 'download_urls.tsv',
                            invalid_oai_items=tmp_path / 'invalid_oai_items.tsv',
                            items_without_files=tmp_path / 'items_without_files.tsv',
                        )
                    )

            self.assertEqual(
                [('1895/Kvali_1895_N1.pdf', 'https://repo.example.org/bitstream/1234/1/1/Kvali_1895_N1.pdf', 123)],
                planned_rows,
            )
            self.assertEqual(1, item_count)
            self.assertEqual(1, invalid_item_count)
            self.assertEqual(0, nofile_count)
            self.assertEqual(1, page_count)
            self.assertEqual(0, collision_count)
            self.assertEqual(
                '1234/2\t404\toai:repo.example.org:1234/2\n',
                (tmp_path / 'invalid_oai_items.tsv').read_text(encoding='utf-8'),
            )


if __name__ == '__main__':
    unittest.main()
