"""Python-stdlib offline tests; no model, network or hardware access."""

from contextlib import redirect_stderr, redirect_stdout
from decimal import Decimal
import io
import json
from pathlib import Path
import subprocess
import sys
import unittest

from memory_floor import (
    DEFAULT_PARAMETERS, ESTIMATE_TYPE, GB_BYTES, GIB_BYTES, main, weight_floor,
)


class WeightFloorTests(unittest.TestCase):
    def test_16_bit_floor(self):
        result = weight_floor(DEFAULT_PARAMETERS, 16)
        self.assertEqual(result["ideal_weight_bytes"], 48_000_000_000)
        self.assertEqual(result["ideal_weight_gb"], 48)
        self.assertAlmostEqual(result["ideal_weight_gib"], 44.70348358154297)

    def test_8_bit_floor(self):
        result = weight_floor(DEFAULT_PARAMETERS, 8)
        self.assertEqual(result["ideal_weight_bytes"], 24_000_000_000)
        self.assertEqual(result["ideal_weight_gb"], 24)
        self.assertAlmostEqual(result["ideal_weight_gib"], 22.351741790771484)

    def test_4_bit_floor(self):
        result = weight_floor(DEFAULT_PARAMETERS, 4)
        self.assertEqual(result["ideal_weight_bytes"], 12_000_000_000)
        self.assertEqual(result["ideal_weight_gb"], 12)
        self.assertAlmostEqual(result["ideal_weight_gib"], 11.175870895385742)

    def test_decimal_gb_unit(self):
        result = weight_floor(GB_BYTES, 8)
        self.assertEqual(result["ideal_weight_gb"], 1)
        self.assertNotEqual(result["ideal_weight_gib"], 1)

    def test_binary_gib_unit(self):
        result = weight_floor(GIB_BYTES, 8)
        self.assertEqual(result["ideal_weight_gib"], 1)
        self.assertEqual(result["ideal_weight_gb"], 1.073741824)

    def test_positive_integral_float_parameters(self):
        self.assertEqual(weight_floor(24e9, 4.0), weight_floor(DEFAULT_PARAMETERS, 4))

    def test_decimal_inputs(self):
        self.assertEqual(
            weight_floor(Decimal("24e9"), Decimal("4")),
            weight_floor(DEFAULT_PARAMETERS, 4),
        )

    def test_fractional_bits_are_ideal_arithmetic(self):
        result = weight_floor(8, Decimal("4.5"))
        self.assertEqual(result["ideal_weight_bytes"], 4.5)

    def test_sub_byte_floor_is_not_an_allocation(self):
        self.assertEqual(weight_floor(1, 4)["ideal_weight_bytes"], 0.5)

    def test_linear_parameter_scaling(self):
        low = weight_floor(12_000_000_000, 8)
        high = weight_floor(DEFAULT_PARAMETERS, 8)
        self.assertEqual(high["ideal_weight_bytes"], 2 * low["ideal_weight_bytes"])

    def test_linear_precision_scaling(self):
        low = weight_floor(DEFAULT_PARAMETERS, 4)
        high = weight_floor(DEFAULT_PARAMETERS, 16)
        self.assertEqual(high["ideal_weight_bytes"], 4 * low["ideal_weight_bytes"])

    def test_nonpositive_values_rejected(self):
        for value in (0, -1, -0.0, Decimal("-1")):
            with self.subTest(value=value), self.assertRaises(ValueError):
                weight_floor(value, 4)
            with self.subTest(bits=value), self.assertRaises(ValueError):
                weight_floor(DEFAULT_PARAMETERS, value)

    def test_nan_and_infinity_rejected(self):
        for value in (
            float("nan"), float("inf"), float("-inf"),
            Decimal("NaN"), Decimal("sNaN"), Decimal("Infinity"),
        ):
            with self.subTest(value=str(value)), self.assertRaises(ValueError):
                weight_floor(value, 4)
            with self.subTest(bits=str(value)), self.assertRaises(ValueError):
                weight_floor(DEFAULT_PARAMETERS, value)

    def test_bool_rejected_in_both_positions(self):
        for value in (True, False):
            with self.subTest(value=value), self.assertRaises(ValueError):
                weight_floor(value, 4)
            with self.subTest(bits=value), self.assertRaises(ValueError):
                weight_floor(DEFAULT_PARAMETERS, value)

    def test_non_numeric_values_rejected(self):
        for value in (None, "24000000000", "NaN", [], {}, 4 + 0j):
            with self.subTest(value=repr(value)), self.assertRaises(ValueError):
                weight_floor(value, 4)
            with self.subTest(bits=repr(value)), self.assertRaises(ValueError):
                weight_floor(DEFAULT_PARAMETERS, value)

    def test_fractional_parameters_rejected(self):
        for value in (1.5, Decimal("24000000000.1")):
            with self.subTest(value=value), self.assertRaises(ValueError):
                weight_floor(value, 4)

    def test_unrepresentable_output_rejected(self):
        for parameters, bits in ((10**400, 4), (DEFAULT_PARAMETERS, Decimal("1e-400"))):
            with self.subTest(parameters=parameters, bits=bits), self.assertRaises(ValueError):
                weight_floor(parameters, bits)


class CommandLineTests(unittest.TestCase):
    def test_default_json_has_limits_and_no_measurements(self):
        stdout = io.StringIO()
        with redirect_stdout(stdout):
            self.assertEqual(main([]), 0)
        payload = json.loads(stdout.getvalue())
        self.assertEqual(payload["estimate_type"], ESTIMATE_TYPE)
        self.assertEqual(payload["inference_status"], "NOT_RUN")
        self.assertIsNone(payload["device_fit"])
        self.assertIsNone(payload["measured_memory_bytes"])
        self.assertEqual(len(payload["results"]), 3)
        self.assertEqual(payload["results"][2]["ideal_weight_gb"], 12)
        self.assertIn("KV cache and other runtime state", payload["exclusions"])
        self.assertEqual(payload["units"], {"GB_bytes": 10**9, "GiB_bytes": 2**30})

    def test_cli_accepts_scientific_parameter_notation(self):
        stdout = io.StringIO()
        with redirect_stdout(stdout):
            main(["--parameters", "24e9", "--bits", "4"])
        payload = json.loads(stdout.getvalue())
        self.assertEqual(len(payload["results"]), 1)
        self.assertEqual(payload["results"][0]["ideal_weight_bytes"], 12_000_000_000)

    def test_cli_rejects_invalid_numbers_with_no_json(self):
        cases = (
            ["--parameters", "NaN"], ["--parameters", "True"],
            ["--parameters", "0"], ["--parameters", "1.5"],
            ["--bits", "NaN"], ["--bits", "inf"], ["--bits", "False"],
            ["--bits", "0"], ["--bits", "-4"],
        )
        for args in cases:
            stdout, stderr = io.StringIO(), io.StringIO()
            with self.subTest(args=args), redirect_stdout(stdout), redirect_stderr(stderr):
                with self.assertRaises(SystemExit) as error:
                    main(args)
            self.assertEqual(error.exception.code, 2)
            self.assertEqual(stdout.getvalue(), "")

    def test_entrypoint_executes_offline(self):
        result = subprocess.run(
            [sys.executable, "-B", str(Path(__file__).with_name("memory_floor.py")),
             "--parameters", "24000000000", "--bits", "16", "8", "4"],
            capture_output=True, text=True, check=False,
        )
        self.assertEqual(result.returncode, 0, result.stderr)
        payload = json.loads(result.stdout)
        self.assertEqual(
            [row["ideal_weight_gb"] for row in payload["results"]], [48, 24, 12],
        )


class IntakeTemplateTests(unittest.TestCase):
    def test_blank_template_is_not_a_run_record(self):
        path = Path(__file__).with_name("deployment-intake.json")
        payload = json.loads(path.read_text(encoding="utf-8"))
        self.assertEqual(payload["status"], "NOT_RUN")
        self.assertEqual(payload["measurement_units"]["memory"], "bytes")
        self.assertTrue(all(value is None for value in payload["measurement_results"].values()))
        self.assertIsNone(payload["artifact"]["repository"])
        self.assertIsNone(payload["runtime"]["version"])
        self.assertIsNone(payload["decision"]["device_fit"])


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