import csv
import io
import json
import subprocess
import sys
import unittest
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path

from verify_join import reconcile


ROOT = Path(__file__).resolve().parent
ORDERS = (ROOT / "orders.csv").read_text()
CHANNELS = (ROOT / "channels.csv").read_text()
REVIEWED = (ROOT / "channels-reviewed.csv").read_text()


class JoinTests(unittest.TestCase):
    def run_example(self, orders=ORDERS, channels=REVIEWED):
        return reconcile(io.StringIO(orders), io.StringIO(channels))

    def test_reviewed_fixture(self):
        self.assertEqual(self.run_example(), {
            "input_order_rows": 4, "input_channel_rows": 4,
            "merged_rows": 4, "currency": "CNY",
            "included_ids": ["S01", "S02", "S03"],
            "excluded_ids": ["S04"], "paid_total": "10.30",
        })

    def test_naive_join_reproduces_inflation(self):
        orders = list(csv.DictReader(io.StringIO(ORDERS)))
        channels = list(csv.DictReader(io.StringIO(CHANNELS)))
        pairs = [(order, channel) for order in orders for channel in channels
                 if order["order_id"] == channel["order_id"]]
        self.assertEqual(len(pairs), 5)
        total = sum((Decimal(order["amount"]) for order, _ in pairs
                     if order["status"] == "paid"), Decimal("0.00"))
        self.assertEqual(str(total), "10.50")

    def test_duplicate_right_key_rejected(self):
        with self.assertRaisesRegex(ValueError, "duplicate_key:channels:S02"):
            self.run_example(channels=CHANNELS)

    def test_conflicting_right_key_rejected(self):
        with self.assertRaisesRegex(ValueError, "duplicate_key:channels:S02"):
            self.run_example(channels=REVIEWED + "S02,desktop\n")

    def test_duplicate_left_key_rejected(self):
        with self.assertRaisesRegex(ValueError, "duplicate_key:orders:S01"):
            self.run_example(orders=ORDERS + "S01,CNY,paid,0.10\n")

    def test_missing_right_key_rejected(self):
        with self.assertRaisesRegex(ValueError, "key_set_mismatch"):
            self.run_example(channels=REVIEWED.replace("S03,desktop\n", ""))

    def test_extra_right_key_rejected(self):
        with self.assertRaisesRegex(ValueError, "key_set_mismatch"):
            self.run_example(channels=REVIEWED + "S99,mobile\n")

    def test_three_decimal_amount_rejected(self):
        with self.assertRaisesRegex(ValueError, "invalid_two_decimal_amount:S01"):
            self.run_example(orders=ORDERS.replace("0.10", "0.105"))

    def test_missing_amount_rejected(self):
        with self.assertRaisesRegex(ValueError, "invalid_two_decimal_amount:S01"):
            self.run_example(orders=ORDERS.replace("paid,0.10", "paid,"))

    def test_nonfinite_scientific_negative_and_unicode_amounts_rejected(self):
        for amount in ["NaN", "Infinity", "1e2", "-1.00", "０.１０", " 0.10", "01.00"]:
            with self.subTest(amount=amount):
                with self.assertRaisesRegex(ValueError, "invalid_two_decimal_amount:S01"):
                    self.run_example(orders=ORDERS.replace("0.10", amount))

    def test_other_currency_rejected(self):
        with self.assertRaisesRegex(ValueError, "unsupported_currency:S01"):
            self.run_example(orders=ORDERS.replace("S01,CNY", "S01,USD"))

    def test_unknown_status_rejected(self):
        with self.assertRaisesRegex(ValueError, "unknown_status:S01"):
            self.run_example(orders=ORDERS.replace("S01,CNY,paid", "S01,CNY,pending"))

    def test_zero_amount_allowed(self):
        self.assertEqual(self.run_example(orders=ORDERS.replace("0.10", "0.00"))["paid_total"], "10.20")

    def test_refunded_is_excluded_without_netting(self):
        result = self.run_example(orders=ORDERS.replace("S03,CNY,paid", "S03,CNY,refunded"))
        self.assertEqual(result["paid_total"], "0.30")
        self.assertEqual(result["excluded_ids"], ["S03", "S04"])

    def test_wrong_header_or_field_count_rejected(self):
        for source in [ORDERS.replace("amount\n", "amount_extra\n"),
                       ORDERS.replace("S01,CNY,paid,0.10", "S01,CNY,paid,0.10,extra"),
                       ORDERS.replace("S01,CNY,paid,0.10", "S01,CNY,paid")]:
            with self.subTest(source=source):
                with self.assertRaises(ValueError):
                    self.run_example(orders=source)

    def test_missing_source_rejected(self):
        for value in ["", "   "]:
            with self.subTest(value=value):
                with self.assertRaisesRegex(ValueError, "missing_source:S01"):
                    self.run_example(channels=REVIEWED.replace("S01,desktop", "S01," + value))

    def test_empty_or_invalid_key_rejected(self):
        with self.assertRaisesRegex(ValueError, "empty_table"):
            self.run_example(channels="order_id,source\n")
        with self.assertRaisesRegex(ValueError, "invalid_order_id:orders"):
            self.run_example(orders=ORDERS.replace("S01,CNY", ",CNY"))

    def test_maximum_bounded_amount_is_exact(self):
        result = self.run_example(orders=ORDERS.replace("10.00", "9999999999999999.99"))
        self.assertEqual(result["paid_total"], "10000000000000000.29")

    def test_rounding_stage_changes_total(self):
        values = [Decimal("0.005"), Decimal("0.005")]
        per_row = sum((value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
                       for value in values), Decimal("0.00"))
        at_total = sum(values).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        self.assertEqual(str(per_row), "0.02")
        self.assertEqual(str(at_total), "0.01")

    def test_cli_returns_distinct_status_codes(self):
        for channels, code, valid in [("channels.csv", 2, False),
                                      ("channels-reviewed.csv", 0, True)]:
            with self.subTest(channels=channels):
                run = subprocess.run([sys.executable, str(ROOT / "verify_join.py"),
                                      str(ROOT / "orders.csv"), str(ROOT / channels)],
                                     capture_output=True, text=True)
                self.assertEqual(run.returncode, code)
                self.assertEqual(json.loads(run.stdout)["valid"], valid)


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