"""
Integration tests for the Amazon purchaseId#workItemId reference format

Unlike test_retry_reclaim.py (which fakes the HTTP layer), these tests run a
real HTTP server standing in for 1Rx and drive the real aiohttp client, the
real JSON mapper, and real zip generation through the full pipeline:

    ingress (process_api_request) -> 1Rx webhook (process_1rx_response)
        -> JPEG download -> response zip -> SFTP upload

They verify, against actual HTTP traffic:
1. The JPEG download URL arrives percent-encoded (%23) with the full path —
   a raw '#' would be dropped client-side as a URL fragment and the server
   would see a truncated path
2. The reference id is echoed byte-for-byte in the generated Amazon response
3. call_1rx_api retries against real 5xx responses
4. A failed request is reclaimed and reprocessed when Amazon retries
"""

import asyncio
import json
import shutil
import sys
import threading
import unittest
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from unittest.mock import MagicMock, patch
from urllib.parse import unquote

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from fastapi import HTTPException

import app.src.file_processor as file_processor_mod
from app.api.consultation_service import ConsultationProcessor, ProcessRequest
from app.src.file_processor import FileProcessor

HASH_REFERENCE_ID = "404-4539392-3665964#6364e0b9-566f-448a-97cb-702ba1c04ae6"
CONSULTATION_ID = "8a7c6d5e-4f3b-2e1d-9c8b-7a6f5e4d3c2b"
JPEG_BYTES = b"\xff\xd8\xff\xe0fake-jpeg-bytes"


class Stub1RxHandler(BaseHTTPRequestHandler):
    """Stub 1Rx server: accepts orders, serves the prescription JPEG"""

    def log_message(self, *args):
        pass

    def _respond(self, status, body, content_type="application/json"):
        self.send_response(status)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_POST(self):
        self.server.requests.append(("POST", self.path))
        self.rfile.read(int(self.headers.get("Content-Length", 0)))

        if self.server.post_failures_remaining > 0:
            self.server.post_failures_remaining -= 1
            self._respond(500, b"1rx unavailable", "text/plain")
            return

        self._respond(202, json.dumps({"data": {"orderId": "ord-1"}}).encode())

    def do_GET(self):
        # self.path preserves the raw (still percent-encoded) request path
        self.server.requests.append(("GET", self.path))

        expected = (
            f"/api/1rx/v1/orders/{self.server.jpeg_order_id}"
            "/download-jpeg-prescription"
        )
        if unquote(self.path) == expected:
            self._respond(200, JPEG_BYTES, "image/jpeg")
        else:
            self._respond(404, b"order not found", "text/plain")


class FakeRequestStore:
    """In-memory stand-in mimicking MySQLRequestStore semantics"""

    def __init__(self):
        self.rows = {}

    def store_request(self, request_data):
        cid = request_data["consultation_id"]
        if cid in self.rows:
            return False
        self.rows[cid] = {**request_data, "status": "pending"}
        return True

    def reclaim_failed_request(self, request_data):
        row = self.rows.get(request_data["consultation_id"])
        if row and row["status"] == "failed_1rx":
            row.update(request_data)
            row["status"] = "pending"
            return True
        return False

    def update_request_status(self, cid, status, error_message=None):
        self.rows[cid]["status"] = status
        self.rows[cid]["last_error"] = error_message

    def get_request_by_consultation_id(self, cid):
        return self.rows.get(cid)


class FakePGP:
    """Copies instead of encrypting so the pipeline stays runnable"""

    def encrypt_file(self, src, dst):
        shutil.copy(src, dst)
        return True


class FakeSFTP:
    """Records uploads"""

    def __init__(self):
        self.uploads = []

    def upload_file(self, local_path, remote_path, username=None):
        self.uploads.append(remote_path)
        return True

    def disconnect(self):
        pass


def make_amazon_request(tracking_id="trk-1"):
    return {
        "messageIdentifiers": {
            "primaryReferenceId": HASH_REFERENCE_ID,
            "secondaryReferenceId": CONSULTATION_ID,
            "trackingId": tracking_id,
            "isTestData": False,
        },
        "data": {
            "requestMetadata": {
                "messageId": "msg-1",
                "messageSentTime": 1699968883231,
                "messageType": "REVALIDATION_REQUEST",
            },
            "consultationDetails": {"sla": 1699968999999, "priority": "P0"},
            "rxDetailForConsultation": [
                {
                    "rxHeaderDetailsForConsultation": {
                        "rxId": "rx-1",
                        "patientContactDetails": {
                            "name": "Test Patient",
                            "contactNumber": "+919999999999",
                        },
                    },
                    "rxItemDetailForConsultation": [
                        {
                            "rxItemId": "CILIDIN_CILNIDIPINE#10.0#MILLIGRAMS###1_TABLETS",
                            "medicineName": "Cilidin Tablet",
                            "orderedMedicine": "https://www.amazon.in/dp/B07X1ZM63M/",
                            "numberOfUnitsOrdered": 10,
                            "dosage": {
                                "dose": 1,
                                "form": "TABLETS",
                                "frequency": "2",
                                "frequencyUnit": "DAILY",
                                "duration": "5",
                                "durationUnit": "DAYS",
                            },
                        }
                    ],
                }
            ],
        },
    }


class TestHashReferenceIdIntegration(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.server = ThreadingHTTPServer(("127.0.0.1", 0), Stub1RxHandler)
        cls.server.requests = []
        cls.server.post_failures_remaining = 0
        cls.server.jpeg_order_id = HASH_REFERENCE_ID
        cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
        cls.thread.start()
        cls.port = cls.server.server_address[1]

    @classmethod
    def tearDownClass(cls):
        cls.server.shutdown()
        cls.server.server_close()

    def setUp(self):
        self.server.requests.clear()
        self.server.post_failures_remaining = 0

        config = MagicMock()
        config.rx_api_key = "test-key"
        config.rx_api_base_url = f"http://127.0.0.1:{self.port}/api/1rx/v1"
        config.rx_api_retry_attempts = 3
        config.rx_api_retry_delay_seconds = 0.01
        config.rx_api_timeout_seconds = 5.0
        config.outgoing_sftp_path = "/consultationresponses"
        config.outgoing_sftp_username = "amazon-out"
        config.sftp_username = "amazon"
        # MagicMock attributes are truthy; pin the egress channels so these
        # tests keep exercising the legacy SFTP flow only.
        config.egress_sftp_enabled = True
        config.egress_api_enabled = False

        self.store = FakeRequestStore()
        with patch.object(file_processor_mod, "SFTPHandler"), patch.object(
            file_processor_mod, "PGPHandler"
        ):
            self.fp = FileProcessor(config, self.store)

        # Capture the Amazon responses produced by the mapper so tests can
        # assert on the payload sent back to Amazon.
        self.amazon_responses = []
        original_map = self.fp.mapper.rx_to_amazon_response

        def _capture(rx_response, original_request):
            response = original_map(rx_response, original_request)
            self.amazon_responses.append(response)
            return response

        self.fp.mapper.rx_to_amazon_response = _capture
        self.fp.pgp = FakePGP()
        self.fake_sftp = FakeSFTP()
        self.fp._create_sftp_handler = lambda: self.fake_sftp

    def make_webhook_response(self, stored_request):
        """1Rx APPROVED webhook consistent with the stored mapped request"""
        medicine = stored_request["rx_request"]["medicines"][0]
        return {
            "status": "APPROVED",
            "pharmacyOrderId": HASH_REFERENCE_ID,
            "shipmentCode": CONSULTATION_ID,
            "orderedMedicines": [
                {"uuid": medicine["uuid"], "url": medicine["url"]}
            ],
            "approvedMedicines": [
                {
                    "id": medicine["id"],
                    "uuid": medicine["uuid"],
                    "name": medicine["name"],
                    "quantity": medicine["quantity"],
                    "dosage": "1-0-1",
                    "type": "TABLETS",
                    "days": 5,
                    "comments": "after food",
                }
            ],
            "doctor": {"name": "Dr. Test", "registrationNumber": "REG123"},
            "patient": {"name": "Test Patient", "age": 30},
        }

    def test_full_flow_jpeg_url_percent_encoded_and_id_echoed(self):
        # Ingress: Amazon sends the consultation request with the '#' id
        result = asyncio.run(self.fp.process_api_request(make_amazon_request()))
        self.assertTrue(result["success"])
        self.assertEqual(
            result["message_identifiers"]["primaryReferenceId"], HASH_REFERENCE_ID
        )
        self.assertEqual(self.store.rows[CONSULTATION_ID]["status"], "sent_to_1rx")

        # Webhook: 1Rx completes the consultation, driven through the
        # ConsultationProcessor which owns the status transitions
        stored = self.store.get_request_by_consultation_id(CONSULTATION_ID)
        webhook = self.make_webhook_response(stored)
        processor = ConsultationProcessor(self.store, self.fp)
        asyncio.run(
            processor.process_1rx_response(
                ProcessRequest(
                    consultation_id=CONSULTATION_ID,
                    webhook_response=webhook,
                    original_request=stored,
                    timestamp=1699968883,
                    source="test",
                )
            )
        )

        # The JPEG request must reach the server percent-encoded and complete.
        # Before the fix the raw '#' was dropped as a URL fragment and the
        # server saw only /orders/404-4539392-3665964 (no download suffix).
        gets = [p for (m, p) in self.server.requests if m == "GET"]
        self.assertEqual(len(gets), 1)
        raw_path = gets[0]
        self.assertNotIn("#", raw_path)
        self.assertIn("%23", raw_path)
        self.assertTrue(raw_path.endswith("/download-jpeg-prescription"))

        # Response was generated and uploaded, with the raw id in the filename
        self.assertEqual(
            self.store.rows[CONSULTATION_ID]["status"], "processing_complete"
        )
        self.assertEqual(len(self.fake_sftp.uploads), 1)
        self.assertIn(HASH_REFERENCE_ID, self.fake_sftp.uploads[0])

        # The Amazon response JSON echoes the reference id byte-for-byte
        self.assertEqual(len(self.amazon_responses), 1)
        amazon_response = self.amazon_responses[0]
        self.assertEqual(
            amazon_response["responseMetadata"]["referenceId"], HASH_REFERENCE_ID
        )
        self.assertEqual(
            amazon_response["responseMetadata"]["consultationId"], CONSULTATION_ID
        )
        self.assertEqual(
            amazon_response["consultationResponse"]["status"], "SUCCESS"
        )

    def test_response_failure_recorded_not_masked_as_success(self):
        # A failure while generating the response (here: PGP encryption)
        # must leave the row failed_processing with the error recorded —
        # previously FileProcessor swallowed it and the row ended up
        # processing_complete with no last_error.
        result = asyncio.run(self.fp.process_api_request(make_amazon_request()))
        self.assertTrue(result["success"])

        class BrokenPGP:
            def encrypt_file(self, src, dst):
                return False

        self.fp.pgp = BrokenPGP()

        stored = self.store.get_request_by_consultation_id(CONSULTATION_ID)
        webhook = self.make_webhook_response(stored)
        processor = ConsultationProcessor(self.store, self.fp)
        with self.assertRaises(HTTPException):
            asyncio.run(
                processor.process_1rx_response(
                    ProcessRequest(
                        consultation_id=CONSULTATION_ID,
                        webhook_response=webhook,
                        original_request=stored,
                        timestamp=1699968883,
                        source="test",
                    )
                )
            )

        row = self.store.rows[CONSULTATION_ID]
        self.assertEqual(row["status"], "failed_processing")
        self.assertIn("encrypt", row["last_error"])
        self.assertEqual(len(self.fake_sftp.uploads), 0)

    def test_ingress_retries_real_5xx_then_succeeds(self):
        self.server.post_failures_remaining = 2

        result = asyncio.run(self.fp.process_api_request(make_amazon_request()))

        self.assertTrue(result["success"])
        posts = [p for (m, p) in self.server.requests if m == "POST"]
        self.assertEqual(len(posts), 3)

    def test_failed_request_reclaimed_on_amazon_retry(self):
        # 1Rx down for all attempts of the first request
        self.server.post_failures_remaining = 10

        first = asyncio.run(self.fp.process_api_request(make_amazon_request("trk-1")))
        self.assertFalse(first["success"])
        self.assertEqual(self.store.rows[CONSULTATION_ID]["status"], "failed_1rx")

        # 1Rx recovers; Amazon retries with a new trackingId
        self.server.post_failures_remaining = 0
        retry = asyncio.run(self.fp.process_api_request(make_amazon_request("trk-2")))

        self.assertTrue(retry["success"])
        self.assertNotIn("Duplicate", retry.get("message", ""))
        self.assertEqual(self.store.rows[CONSULTATION_ID]["status"], "sent_to_1rx")


if __name__ == "__main__":
    unittest.main()
