import argparse
import importlib.util
import sys
import unittest
from decimal import Decimal
from pathlib import Path


CANDIDATE_DIR = Path(__file__).parent / "reference"
sys.dont_write_bytecode = True


def load_candidate(filename):
    path = CANDIDATE_DIR / filename
    spec = importlib.util.spec_from_file_location(f"reviewed_{path.stem}", path)
    if spec is None or spec.loader is None:
        raise RuntimeError("candidate cannot be loaded")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


class QuantityContractTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.module = load_candidate("quantity.py")

    def test_exact_decimal_and_tuple(self):
        result = self.module.parse_quantity("1.250 kg")
        self.assertIs(type(result), tuple)
        self.assertEqual(len(result), 2)
        self.assertIs(type(result[0]), Decimal)
        self.assertEqual(result[0].as_tuple(), Decimal("1.250").as_tuple())
        self.assertEqual(result[1], "kg")

    def test_signs_and_chinese_units(self):
        self.assertEqual(self.module.parse_quantity("-0.05g"), (Decimal("-0.05"), "g"))
        self.assertEqual(self.module.parse_quantity(" +12 个 "), (Decimal("12"), "个"))

    def test_zero_and_whitespace(self):
        self.assertEqual(self.module.parse_quantity("\t0 g\n"), (Decimal("0"), "g"))
        self.assertEqual(self.module.parse_quantity("0012 g"), (Decimal("12"), "g"))

    def test_precision_is_not_float(self):
        self.assertEqual(self.module.parse_quantity("12345678901234567890.001kg"), (Decimal("12345678901234567890.001"), "kg"))

    def test_non_string_rejected(self):
        for value in (True, 3, None, 1.25, ["1kg"]):
            with self.subTest(value=value), self.assertRaises(TypeError):
                self.module.parse_quantity(value)

    def test_partial_or_special_numbers_rejected(self):
        for value in ("NaNkg", "1e3g", "1,000g", "2 kg approx", "kg", "", ".5g", "1.g", "1KG", "１kg", "٢g", "1\u200bkg"):
            with self.subTest(value=value), self.assertRaises(ValueError):
                self.module.parse_quantity(value)


class RetryContractTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.module = load_candidate("retry.py")

    def test_zero_has_no_delay(self):
        self.assertEqual(self.module.retry_delays(0, 100, 1000), [])

    def test_first_delay_starts_at_base(self):
        self.assertEqual(self.module.retry_delays(4, 100, 250), [100, 200, 250, 250])

    def test_cap_and_max_attempts(self):
        self.assertEqual(self.module.retry_delays(6, 1000, 5000), [1000, 2000, 4000, 5000, 5000, 5000])
        self.assertEqual(self.module.retry_delays(3, 500, 500), [500, 500, 500])

    def test_output_is_exact_integer_list(self):
        result = self.module.retry_delays(2, 1, 60000)
        self.assertIs(type(result), list)
        self.assertTrue(all(type(value) is int for value in result))
        self.assertEqual(result, [1, 2])

    def test_invalid_ranges_rejected(self):
        for values in ((-1, 100, 1000), (7, 100, 1000), (2, 0, 100), (2, 100, 50), (2, 100, 60001)):
            with self.subTest(values=values), self.assertRaises(ValueError):
                self.module.retry_delays(*values)

    def test_bool_float_and_string_rejected(self):
        for values in ((True, 100, 1000), (2, 100.0, 1000), (2, 100, False), ("2", 100, 1000)):
            with self.subTest(values=values), self.assertRaises(TypeError):
                self.module.retry_delays(*values)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Run only after manually reviewing candidate files.")
    parser.add_argument("--candidate-dir", type=Path)
    args, remaining = parser.parse_known_args()
    if args.candidate_dir is not None:
        CANDIDATE_DIR = args.candidate_dir.resolve()
    unittest.main(argv=[sys.argv[0], *remaining])
