"""
Tests for egress channel routing (Step 5 of the Consume API integration)

These tests verify:
1. EGRESS_MODE routing: sftp-only never touches the API, both delivers
   SFTP first and an API failure never breaks processing, api-only skips
   SFTP and raises on API failure
2. The API delivery stages the ZIP, builds the v2 payload with a fresh
   trackingId and records the outcome (acknowledged / errored /
   failed_to_build) on the row
3. The management resend flow rebuilds from the stored request +
   rx_response and sends with a new trackingId
"""

import asyncio
import sys
import unittest
import uuid
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

import app.src.file_processor as file_processor_mod
from app.src.amazon_consume_client import DeliveryOutcome, DeliveryResult
from app.src.file_processor import FileProcessor

PRIMARY = "404-1234567-1234567"
CONSULTATION_ID = "cons-123"
SIGNED_URL = "https://bucket.s3.amazonaws.com/rx.zip?sig=abc"


class MockConfig:
    # test_orders_only defaults to False here so the EGRESS_MODE tests below
    # exercise the mode semantics alone; TestTestOrderGate turns it on.
    def __init__(self, egress_mode="both", test_orders_only=False, real_order_limit=0):
        self.egress_mode = egress_mode
        self.egress_sftp_enabled = egress_mode in ("sftp", "both")
        self.egress_api_enabled = egress_mode in ("both", "api")
        self.egress_api_test_orders_only = test_orders_only
        self.egress_api_real_order_limit = real_order_limit
        self.amazon_provider_name = "myrx"
        self.rx_api_key = "test-api-key"
        self.rx_api_base_url = "http://localhost:10000/api/1rx/v1"


def original_request(is_test_data=False):
    return {
        "messageIdentifiers": {
            "primaryReferenceId": PRIMARY,
            "secondaryReferenceId": CONSULTATION_ID,
            "trackingId": "amazon-trk-1",
            "isTestData": is_test_data,
        },
        "data": {
            "rxDetailForConsultation": [
                {
                    "rxHeaderDetailsForConsultation": {
                        "rxId": "rx-1",
                        "patientContactDetails": {"name": "Test Patient"},
                    },
                    "rxItemDetailForConsultation": [
                        {
                            "rxItemId": "ITEM-1",
                            "orderedMedicine": "https://www.amazon.in/dp/B07X1ZM63M/",
                        }
                    ],
                }
            ],
        },
    }


def approved_rx_response():
    return {
        "status": "APPROVED",
        "pharmacyOrderId": PRIMARY,
        "orderedMedicines": [{"uuid": "u-1", "url": "https://www.amazon.in/dp/B07X1ZM63M/"}],
        "approvedMedicines": [
            {
                "id": "ITEM-1",
                "uuid": "u-1",
                "name": "Some medicine",
                "quantity": 10,
                "dosage": "1-0-1",
                "type": "TABLETS",
                "days": 5,
            }
        ],
        "doctor": {
            "name": "Dr. A",
            "phone": "+91-9876543210",
            "registrationNumber": "MCI-1",
            "address": "Bangalore",
        },
        "patient": {"name": "John Doe"},
    }


def failed_rx_response():
    return {"status": "CANCELLED", "orderCancelReason": "Call not answered"}


def stored_request(rx_response=None, is_test_data=None, request_is_test_data=False):
    """is_test_data mirrors the DB column (1/0/None); request_is_test_data the
    isTestData flag inside the stored original request."""
    stored = {
        "consultation_id": CONSULTATION_ID,
        "reference_id": PRIMARY,
        "original_request": original_request(request_is_test_data),
    }
    if is_test_data is not None:
        stored["is_test_data"] = is_test_data
    if rx_response is not None:
        stored["rx_response"] = rx_response
    return stored


def acknowledged():
    return DeliveryResult(
        outcome=DeliveryOutcome.ACKNOWLEDGED,
        http_status=200,
        acknowledgement_timestamp=1699968883231,
        attempts=1,
    )


def errored(message="bad payload"):
    return DeliveryResult(
        outcome=DeliveryOutcome.ERRORED, http_status=200, error_message=message
    )


class EgressTestCase(unittest.TestCase):
    def make_fp(self, egress_mode, test_orders_only=False, real_order_limit=0, delivered=0):
        self.store = MagicMock()
        self.store.count_real_orders_delivered_via_api.return_value = delivered
        with patch.object(file_processor_mod, "SFTPHandler"), patch.object(
            file_processor_mod, "PGPHandler"
        ):
            fp = FileProcessor(
                MockConfig(egress_mode, test_orders_only, real_order_limit), self.store
            )

        async def fake_download(rx_response, jpeg_path):
            Path(jpeg_path).write_bytes(b"jpeg")
            return True

        fp.download_prescription_jpeg = AsyncMock(side_effect=fake_download)
        fp.generate_and_upload_response = AsyncMock()
        fp.upload_prescription_zip = AsyncMock(
            return_value=(SIGNED_URL, f"{CONSULTATION_ID}.jpg")
        )
        self.client = MagicMock()
        self.client.send_consultation_response = AsyncMock(return_value=acknowledged())
        fp._consume_client = self.client
        return fp

    def process(self, fp, rx_response=None, stored=None):
        asyncio.run(
            fp.process_1rx_response(
                stored or stored_request(), rx_response or approved_rx_response()
            )
        )

    def sent_payload(self):
        return self.client.send_consultation_response.call_args[0][0]


class TestChannelRouting(EgressTestCase):
    def test_sftp_mode_never_calls_the_api(self):
        fp = self.make_fp("sftp")

        self.process(fp)

        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_not_called()
        fp.upload_prescription_zip.assert_not_called()

    def test_both_mode_delivers_to_sftp_and_api(self):
        fp = self.make_fp("both")

        self.process(fp)

        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_awaited_once()
        self.assertEqual(fp.download_prescription_jpeg.await_count, 1)

    def test_api_mode_skips_sftp(self):
        fp = self.make_fp("api")

        self.process(fp)

        fp.generate_and_upload_response.assert_not_called()
        self.client.send_consultation_response.assert_awaited_once()

    def test_both_mode_api_failure_does_not_break_processing(self):
        fp = self.make_fp("both")
        self.client.send_consultation_response.return_value = errored()

        self.process(fp)  # must not raise

        fp.generate_and_upload_response.assert_awaited_once()
        self.store.record_egress_api_result.assert_called_once()
        _, _, status, error = self.store.record_egress_api_result.call_args[0]
        self.assertEqual(status, "errored")
        self.assertIn("bad payload", error)

    def test_api_mode_failure_raises(self):
        fp = self.make_fp("api")
        self.client.send_consultation_response.return_value = errored()

        with self.assertRaises(Exception) as ctx:
            self.process(fp)
        self.assertIn("bad payload", str(ctx.exception))

    def test_rx_response_is_persisted_for_resend(self):
        fp = self.make_fp("both")

        self.process(fp)

        self.store.save_rx_response.assert_called_once()
        cid, saved = self.store.save_rx_response.call_args[0]
        self.assertEqual(cid, CONSULTATION_ID)
        self.assertEqual(saved["status"], "APPROVED")


class TestTestOrderGate(EgressTestCase):
    """EGRESS_API_TEST_ORDERS_ONLY: Amazon accepts API responses for test
    orders only, so real orders fall back to SFTP whatever EGRESS_MODE says."""

    def test_api_mode_real_order_goes_to_sftp_only(self):
        fp = self.make_fp("api", test_orders_only=True)

        self.process(fp, stored=stored_request(is_test_data=0))

        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_not_called()
        fp.upload_prescription_zip.assert_not_called()
        self.store.record_egress_api_result.assert_not_called()

    def test_api_mode_test_order_goes_to_api_only(self):
        fp = self.make_fp("api", test_orders_only=True)

        self.process(
            fp, stored=stored_request(is_test_data=1, request_is_test_data=True)
        )

        fp.generate_and_upload_response.assert_not_called()
        self.client.send_consultation_response.assert_awaited_once()
        self.assertIs(self.sent_payload()["messageIdentifiers"]["isTestData"], True)

    def test_both_mode_real_order_skips_api(self):
        fp = self.make_fp("both", test_orders_only=True)

        self.process(fp, stored=stored_request(is_test_data=0))

        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_not_called()

    def test_both_mode_test_order_delivers_to_both(self):
        fp = self.make_fp("both", test_orders_only=True)

        self.process(fp, stored=stored_request(is_test_data=1))

        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_awaited_once()

    def test_sftp_mode_test_order_still_never_calls_the_api(self):
        fp = self.make_fp("sftp", test_orders_only=True)

        self.process(fp, stored=stored_request(is_test_data=1))

        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_not_called()

    def test_falls_back_to_request_flag_when_column_is_missing(self):
        # Rows stored before is_test_data existed, or fetched without it
        fp = self.make_fp("api", test_orders_only=True)

        self.process(fp, stored=stored_request(request_is_test_data=True))

        fp.generate_and_upload_response.assert_not_called()
        self.client.send_consultation_response.assert_awaited_once()

    def test_legacy_sftp_request_without_flag_is_a_real_order(self):
        fp = self.make_fp("api", test_orders_only=True)
        stored = stored_request()
        stored["original_request"] = {
            "requestMetadata": {"referenceId": PRIMARY, "consultationId": CONSULTATION_ID},
            "rxDetailForConsultation": [],
        }

        self.process(fp, stored=stored)

        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_not_called()

    def test_real_order_sftp_failure_raises(self):
        # With the API skipped, SFTP is the only channel: its failure must
        # surface so the caller records failed_processing.
        fp = self.make_fp("api", test_orders_only=True)
        fp.generate_and_upload_response.side_effect = Exception("sftp upload failed")

        with self.assertRaises(Exception) as ctx:
            self.process(fp, stored=stored_request(is_test_data=0))
        self.assertIn("sftp upload failed", str(ctx.exception))
        self.client.send_consultation_response.assert_not_called()

    def test_gate_off_sends_real_order_to_api(self):
        fp = self.make_fp("api", test_orders_only=False)

        self.process(fp, stored=stored_request(is_test_data=0))

        fp.generate_and_upload_response.assert_not_called()
        self.client.send_consultation_response.assert_awaited_once()


class TestRealOrderRamp(EgressTestCase):
    """EGRESS_API_REAL_ORDER_LIMIT: with the test-orders gate on, the first N
    real orders acknowledged over the API still go via the API; after that
    real orders drop back to SFTP. Test orders are unaffected."""

    def test_real_order_within_ramp_goes_to_api_only(self):
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=100, delivered=42)

        self.process(fp, stored=stored_request(is_test_data=0))

        self.client.send_consultation_response.assert_awaited_once()
        fp.generate_and_upload_response.assert_not_called()
        self.store.record_egress_api_result.assert_called_once()
        self.assertEqual(self.store.record_egress_api_result.call_args[0][2], "acknowledged")

    def test_real_order_beyond_ramp_goes_to_sftp_only(self):
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=100, delivered=100)

        self.process(fp, stored=stored_request(is_test_data=0))

        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_not_called()
        self.store.record_egress_api_result.assert_not_called()

    def test_ramp_counts_acknowledged_deliveries_from_the_store(self):
        # Overshoot in the store (e.g. concurrent webhooks) must still close the ramp
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=100, delivered=101)

        self.process(fp, stored=stored_request(is_test_data=0))

        self.client.send_consultation_response.assert_not_called()
        fp.generate_and_upload_response.assert_awaited_once()

    def test_zero_limit_keeps_real_orders_on_sftp_without_counting(self):
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=0, delivered=0)

        self.process(fp, stored=stored_request(is_test_data=0))

        self.client.send_consultation_response.assert_not_called()
        self.store.count_real_orders_delivered_via_api.assert_not_called()
        fp.generate_and_upload_response.assert_awaited_once()

    def test_ramp_api_failure_falls_back_to_sftp_without_raising(self):
        # A real customer must never lose a response to an API failure.
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=100, delivered=0)
        self.client.send_consultation_response.return_value = errored("scripted rejection")

        self.process(fp, stored=stored_request(is_test_data=0))

        self.client.send_consultation_response.assert_awaited_once()
        fp.generate_and_upload_response.assert_awaited_once()
        self.assertEqual(self.store.record_egress_api_result.call_args[0][2], "errored")

    def test_ramp_unbuildable_payload_falls_back_to_sftp(self):
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=100, delivered=0)
        fp.upload_prescription_zip.side_effect = Exception("s3 down")

        self.process(fp, stored=stored_request(is_test_data=0))

        self.client.send_consultation_response.assert_not_called()
        fp.generate_and_upload_response.assert_awaited_once()
        self.assertEqual(self.store.record_egress_api_result.call_args[0][2], "failed_to_build")

    def test_ramp_sftp_fallback_failure_still_raises(self):
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=100, delivered=0)
        self.client.send_consultation_response.return_value = errored()
        fp.generate_and_upload_response.side_effect = Exception("sftp upload failed")

        with self.assertRaises(Exception) as ctx:
            self.process(fp, stored=stored_request(is_test_data=0))
        self.assertIn("sftp upload failed", str(ctx.exception))

    def test_ramp_in_both_mode_delivers_sftp_then_api_once(self):
        fp = self.make_fp("both", test_orders_only=True, real_order_limit=100, delivered=0)
        self.client.send_consultation_response.return_value = errored()

        self.process(fp, stored=stored_request(is_test_data=0))

        # SFTP copy already went out first; no second SFTP upload on API failure
        fp.generate_and_upload_response.assert_awaited_once()
        self.client.send_consultation_response.assert_awaited_once()

    def test_unreadable_count_keeps_real_order_on_sftp(self):
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=100)
        self.store.count_real_orders_delivered_via_api.side_effect = Exception("db down")

        self.process(fp, stored=stored_request(is_test_data=0))

        self.client.send_consultation_response.assert_not_called()
        fp.generate_and_upload_response.assert_awaited_once()

    def test_test_orders_ignore_the_ramp(self):
        fp = self.make_fp("api", test_orders_only=True, real_order_limit=100, delivered=100)

        self.process(fp, stored=stored_request(is_test_data=1))

        self.client.send_consultation_response.assert_awaited_once()
        fp.generate_and_upload_response.assert_not_called()
        self.store.count_real_orders_delivered_via_api.assert_not_called()

    def test_gate_off_ignores_the_ramp(self):
        fp = self.make_fp("api", test_orders_only=False, real_order_limit=1, delivered=5)

        self.process(fp, stored=stored_request(is_test_data=0))

        self.client.send_consultation_response.assert_awaited_once()
        self.store.count_real_orders_delivered_via_api.assert_not_called()


class TestApiDelivery(EgressTestCase):
    def test_payload_carries_signed_url_and_fresh_tracking_id(self):
        fp = self.make_fp("api")

        self.process(fp)

        payload = self.sent_payload()
        ids = payload["messageIdentifiers"]
        self.assertEqual(ids["primaryReferenceId"], PRIMARY)
        self.assertEqual(ids["providerName"], "myrx")
        uuid.UUID(ids["trackingId"])  # a fresh UUID, not Amazon's
        self.assertNotEqual(ids["trackingId"], "amazon-trk-1")
        images = payload["data"]["rxDetailFromConsultation"][0]["rxImageDetails"]
        self.assertEqual(images[0]["fileUrl"], SIGNED_URL)
        self.assertEqual(images[0]["formatType"], "ZIP")
        self.assertEqual(images[0]["fileName"], f"{CONSULTATION_ID}.jpg")

        self.store.record_egress_api_result.assert_called_once()
        cid, tracking_id, status, error = self.store.record_egress_api_result.call_args[0]
        self.assertEqual((cid, status, error), (CONSULTATION_ID, "acknowledged", None))
        self.assertEqual(tracking_id, ids["trackingId"])

    def test_failed_consultation_sends_without_prescription(self):
        fp = self.make_fp("api")

        self.process(fp, failed_rx_response())

        fp.upload_prescription_zip.assert_not_called()
        payload = self.sent_payload()
        self.assertEqual(
            payload["data"]["consultationResponse"]["status"], "FAILED"
        )
        self.assertNotIn("rxDetailFromConsultation", payload["data"])

    def test_unbuildable_payload_is_recorded_and_raises_in_api_mode(self):
        fp = self.make_fp("api")
        rx_response = approved_rx_response()
        del rx_response["doctor"]["address"]

        with self.assertRaises(Exception) as ctx:
            self.process(fp, rx_response)

        self.assertIn("prescriberDetails.address", str(ctx.exception))
        self.client.send_consultation_response.assert_not_called()
        _, _, status, error = self.store.record_egress_api_result.call_args[0]
        self.assertEqual(status, "failed_to_build")
        self.assertIn("prescriberDetails.address", error)


class TestResend(EgressTestCase):
    def test_resend_rebuilds_and_sends_with_new_tracking_id(self):
        fp = self.make_fp("both")
        self.store.get_request_by_consultation_id.return_value = stored_request(
            approved_rx_response()
        )

        result = asyncio.run(fp.resend_via_amazon_api(CONSULTATION_ID))

        self.assertTrue(result["acknowledged"])
        self.assertEqual(result["outcome"], "acknowledged")
        payload = self.sent_payload()
        uuid.UUID(payload["messageIdentifiers"]["trackingId"])
        fp.generate_and_upload_response.assert_not_called()  # API only, no SFTP

    def test_resend_reports_failure_without_raising(self):
        fp = self.make_fp("both")
        self.client.send_consultation_response.return_value = errored()
        self.store.get_request_by_consultation_id.return_value = stored_request(
            approved_rx_response()
        )

        result = asyncio.run(fp.resend_via_amazon_api(CONSULTATION_ID))

        self.assertFalse(result["acknowledged"])
        self.assertEqual(result["outcome"], "errored")

    def test_resend_unknown_consultation_raises_lookup_error(self):
        fp = self.make_fp("both")
        self.store.get_request_by_consultation_id.return_value = None

        with self.assertRaises(LookupError):
            asyncio.run(fp.resend_via_amazon_api(CONSULTATION_ID))

    def test_resend_without_stored_rx_response_raises_value_error(self):
        fp = self.make_fp("both")
        self.store.get_request_by_consultation_id.return_value = stored_request()

        with self.assertRaises(ValueError):
            asyncio.run(fp.resend_via_amazon_api(CONSULTATION_ID))

    def test_resend_with_api_disabled_raises_value_error(self):
        fp = self.make_fp("sftp")

        with self.assertRaises(ValueError):
            asyncio.run(fp.resend_via_amazon_api(CONSULTATION_ID))


if __name__ == "__main__":
    unittest.main()
