"""
Tests for prescription staging (Step 4 of the Consume API integration)

These tests verify:
1. S3Handler uploads to the configured bucket and pre-signs the same key
2. upload_prescription_zip zips the downloaded JPEG, stages it under
   {prefix}/{reference_id}/{consultation_id}.zip and returns the URL +
   file name; S3 failures raise instead of returning a bad payload
3. process_1rx_response downloads the JPEG once and hands the same file
   to the SFTP path (which no longer re-downloads when given one)
"""

import asyncio
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

import app.src.handlers as handlers_mod
import app.src.file_processor as file_processor_mod
from app.src.file_processor import FileProcessor
from app.src.handlers import S3Handler


class MockConfig:
    def __init__(self):
        self.rx_api_key = "test-api-key"
        self.rx_api_base_url = "http://localhost:10000/api/1rx/v1"
        self.outgoing_sftp_path = "/consultationresponses"
        self.outgoing_sftp_username = "amazon-out"
        self.sftp_username = "amazon"
        self.s3_bucket = "rx-bucket"
        self.s3_region = "ap-south-1"
        self.s3_prefix = "consultation-responses"
        self.s3_presign_ttl_seconds = 43200


def make_file_processor(config, request_store=None):
    with patch.object(file_processor_mod, "SFTPHandler"), patch.object(
        file_processor_mod, "PGPHandler"
    ):
        return FileProcessor(config, request_store or MagicMock())


class TestS3Handler(unittest.TestCase):
    def setUp(self):
        self.boto_client = MagicMock()
        with patch.object(
            handlers_mod.boto3, "client", return_value=self.boto_client
        ) as client_factory:
            self.handler = S3Handler(MockConfig())
        client_factory.assert_called_once_with("s3", region_name="ap-south-1")

    def test_upload_targets_configured_bucket(self):
        ok = self.handler.upload_file("/tmp/rx.zip", "prefix/rx.zip")

        self.assertTrue(ok)
        self.boto_client.upload_file.assert_called_once_with(
            "/tmp/rx.zip", "rx-bucket", "prefix/rx.zip"
        )

    def test_upload_failure_returns_false(self):
        self.boto_client.upload_file.side_effect = RuntimeError("denied")

        self.assertFalse(self.handler.upload_file("/tmp/rx.zip", "k"))

    def test_presign_uses_same_bucket_key_and_ttl(self):
        self.boto_client.generate_presigned_url.return_value = "https://signed"

        url = self.handler.generate_presigned_url("prefix/rx.zip", 43200)

        self.assertEqual(url, "https://signed")
        self.boto_client.generate_presigned_url.assert_called_once_with(
            "get_object",
            Params={"Bucket": "rx-bucket", "Key": "prefix/rx.zip"},
            ExpiresIn=43200,
        )

    def test_presign_failure_returns_none(self):
        self.boto_client.generate_presigned_url.side_effect = RuntimeError("no creds")

        self.assertIsNone(self.handler.generate_presigned_url("k", 60))


class TestUploadPrescriptionZip(unittest.TestCase):
    STORED = {
        "consultation_id": "cons-123",
        "reference_id": "404-1234567#a1b2",
    }

    def setUp(self):
        self.fp = make_file_processor(MockConfig())
        self.s3 = MagicMock()
        self.s3.upload_file.return_value = True
        self.s3.generate_presigned_url.return_value = "https://signed-url"
        self.fp._create_s3_handler = lambda: self.s3

    def run_upload(self, stored=None):
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_path = Path(temp_dir)
            jpeg_path = temp_path / "cons-123.jpg"
            jpeg_path.write_bytes(b"jpeg-bytes")
            result = asyncio.run(
                self.fp.upload_prescription_zip(
                    stored or self.STORED, jpeg_path, temp_path
                )
            )
            zip_path = temp_path / "cons-123.zip"
            names = zipfile.ZipFile(zip_path).namelist() if zip_path.exists() else []
        return result, names

    def test_zips_jpeg_and_returns_presigned_url(self):
        (file_url, file_name), zip_names = self.run_upload()

        self.assertEqual(file_url, "https://signed-url")
        self.assertEqual(file_name, "cons-123.jpg")
        self.assertEqual(zip_names, ["cons-123.jpg"])

    def test_key_is_prefix_reference_and_consultation(self):
        self.run_upload()

        expected_key = "consultation-responses/404-1234567#a1b2/cons-123.zip"
        upload_path, key = self.s3.upload_file.call_args[0]
        self.assertEqual(key, expected_key)
        self.assertTrue(upload_path.endswith("cons-123.zip"))
        self.s3.generate_presigned_url.assert_called_once_with(expected_key, 43200)

    def test_empty_prefix_leaves_no_leading_slash(self):
        self.fp.config.s3_prefix = ""

        self.run_upload()

        _, key = self.s3.upload_file.call_args[0]
        self.assertEqual(key, "404-1234567#a1b2/cons-123.zip")

    def test_upload_failure_raises(self):
        self.s3.upload_file.return_value = False

        with self.assertRaises(Exception) as ctx:
            self.run_upload()
        self.assertIn("upload prescription ZIP", str(ctx.exception))
        self.s3.generate_presigned_url.assert_not_called()

    def test_presign_failure_raises(self):
        self.s3.generate_presigned_url.return_value = None

        with self.assertRaises(Exception) as ctx:
            self.run_upload()
        self.assertIn("pre-sign", str(ctx.exception))


class TestSingleJpegDownload(unittest.TestCase):
    STORED = {
        "consultation_id": "cons-123",
        "reference_id": "404-1234567#a1b2",
        "original_request": {"messageIdentifiers": {"primaryReferenceId": "404"}},
    }

    def setUp(self):
        self.fp = make_file_processor(MockConfig())
        self.fp.mapper = MagicMock()
        self.fp.mapper.rx_to_amazon_response.return_value = {"responseMetadata": {}}

    def test_approved_downloads_once_and_passes_path_to_sftp_flow(self):
        async def fake_download(rx_response, jpeg_path):
            Path(jpeg_path).write_bytes(b"jpeg")
            return True

        self.fp.download_prescription_jpeg = AsyncMock(side_effect=fake_download)
        self.fp.generate_and_upload_response = AsyncMock()

        asyncio.run(
            self.fp.process_1rx_response(self.STORED, {"status": "APPROVED"})
        )

        self.assertEqual(self.fp.download_prescription_jpeg.await_count, 1)
        jpeg_path = self.fp.generate_and_upload_response.call_args.kwargs["jpeg_path"]
        self.assertIsNotNone(jpeg_path)
        self.assertTrue(str(jpeg_path).endswith("cons-123.jpg"))

    def test_download_failure_raises_before_any_delivery(self):
        self.fp.download_prescription_jpeg = AsyncMock(return_value=False)
        self.fp.generate_and_upload_response = AsyncMock()

        with self.assertRaises(Exception):
            asyncio.run(
                self.fp.process_1rx_response(self.STORED, {"status": "APPROVED"})
            )

        self.fp.generate_and_upload_response.assert_not_called()

    def test_non_approved_skips_download(self):
        self.fp.download_prescription_jpeg = AsyncMock()
        self.fp.generate_and_upload_response = AsyncMock()

        asyncio.run(
            self.fp.process_1rx_response(self.STORED, {"status": "CANCELLED"})
        )

        self.fp.download_prescription_jpeg.assert_not_called()
        self.assertIsNone(
            self.fp.generate_and_upload_response.call_args.kwargs["jpeg_path"]
        )

    def test_sftp_flow_does_not_redownload_when_given_the_jpeg(self):
        self.fp.download_prescription_jpeg = AsyncMock()
        self.fp.pgp = MagicMock()
        self.fp.pgp.encrypt_file.return_value = True
        sftp = MagicMock()
        sftp.upload_file.return_value = True
        self.fp._create_sftp_handler = lambda: sftp

        with tempfile.TemporaryDirectory() as temp_dir:
            jpeg_path = Path(temp_dir) / "cons-123.jpg"
            jpeg_path.write_bytes(b"jpeg")
            asyncio.run(
                self.fp.generate_and_upload_response(
                    self.STORED,
                    {"responseMetadata": {}},
                    {"status": "APPROVED"},
                    jpeg_path=jpeg_path,
                )
            )

        self.fp.download_prescription_jpeg.assert_not_called()
        sftp.upload_file.assert_called_once()


if __name__ == "__main__":
    unittest.main()
