from __future__ import annotations

import json
import tempfile
import unittest
from pathlib import Path
from shutil import copy2

from core.software_contracts import (
    CONTRACT_ROOT,
    DEFAULT_CAPABILITIES,
    _ContractRegistry,
    get_contract,
    node_available_slots,
    node_supports_request,
    version_at_least,
)


def _request(plot_type: str) -> dict:
    roles = {"input": "sample", "x": "x", "y": "y"}
    if plot_type in {"contour", "surface_3d", "ternary", "heatmap"}:
        roles["z"] = "z"
    return {
        "schema_version": 2,
        "inputs": [{
            "key": "sample",
            "artifact_id": "f4186347-65cc-4f07-9c26-bf11992beef8",
        }],
        "operation": {"plot": {"type": plot_type, "series": [roles]}},
        "outputs": [{"key": "figure_png", "type": "figure", "format": "png"}],
    }


class SoftwareContractTests(unittest.TestCase):
    def test_origin_contract_drives_schema_outputs_and_summary(self) -> None:
        contract = get_contract("origin.plot@v2")
        request = _request("line")
        normalized, digest = contract.normalize_request(request)
        self.assertEqual(normalized, request)
        self.assertEqual(len(digest), 64)
        self.assertEqual(contract.output_namespace, "origin")
        self.assertEqual(contract.workspace.state_output, "project")
        self.assertEqual(contract.workspace.preview_outputs, ("figure_png",))
        self.assertEqual(contract.workspace.required_adapter_version, "1.1.0")
        self.assertEqual(
            set(contract.expected_outputs(request)),
            {"project", "figure_png", "plot_spec", "provenance"},
        )
        self.assertEqual(DEFAULT_CAPABILITIES, ("origin.plot@v2",))

    def test_adapter_version_and_features_gate_additive_requests(self) -> None:
        contract = get_contract("origin.plot@v2")
        legacy_runtime = {
            "available_slots": 1,
            "origin": {"health": "ready", "adapter_version": "0.3.0"},
        }
        self.assertFalse(node_supports_request(contract, _request("line"), legacy_runtime))
        self.assertFalse(node_supports_request(contract, _request("heatmap"), legacy_runtime))
        current_runtime = {
            "capability_runtime": {
                "origin.plot@v2": {
                    "health": "ready",
                    "available_slots": 1,
                    "adapter_version": "1.1.0",
                    "workspace_protocol_version": 1,
                    "features": ["line", "heatmap"],
                }
            }
        }
        self.assertTrue(node_supports_request(contract, _request("heatmap"), current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"]["adapter_version"] = "1.0.1"
        self.assertFalse(node_supports_request(contract, _request("line"), current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"]["adapter_version"] = "1.1.0"
        self.assertEqual(node_available_slots("origin.plot@v2", current_runtime), 1)
        self.assertFalse(node_supports_request(contract, _request("bar"), current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"].pop(
            "workspace_protocol_version"
        )
        self.assertFalse(node_supports_request(contract, _request("line"), current_runtime))
        self.assertEqual(node_available_slots("origin.plot@v2", current_runtime), 0)
        current_runtime["capability_runtime"]["origin.plot@v2"][
            "workspace_protocol_version"
        ] = 1

        multi_panel = _request("line")
        multi_panel["operation"]["plot"] = {
            "type": "multi_panel",
            "layout": {"rows": 1, "columns": 1},
            "panels": [{
                "key": "main",
                "series": [{
                    "input": "sample", "x": "x", "y": "y", "kind": "line",
                }],
            }],
        }
        normalized, _ = contract.normalize_request(multi_panel)
        self.assertEqual(normalized, multi_panel)
        current_runtime["capability_runtime"]["origin.plot@v2"].update({
            "adapter_version": "0.6.0",
            "features": ["multi_panel"],
        })
        self.assertFalse(node_supports_request(contract, multi_panel, current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"].update({
            "adapter_version": "1.1.0",
        })
        self.assertTrue(node_supports_request(contract, multi_panel, current_runtime))

        recipe = json.loads(json.dumps(multi_panel))
        recipe["operation"]["plot"].update({
            "type": "recipe",
            "recipe_version": 1,
        })
        normalized, _ = contract.normalize_request(recipe)
        self.assertEqual(normalized, recipe)
        current_runtime["capability_runtime"]["origin.plot@v2"].update({
            "adapter_version": "0.8.1",
            "features": ["recipe"],
        })
        self.assertFalse(node_supports_request(contract, recipe, current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"].update({
            "adapter_version": "1.1.0",
        })
        self.assertTrue(node_supports_request(contract, recipe, current_runtime))

        recipe["operation"]["plot"].pop("recipe_version")
        with self.assertRaises(ValueError):
            contract.normalize_request(recipe)

        bubble = _request("line")
        bubble["operation"]["plot"] = {
            "type": "bubble",
            "series": [{
                "input": "sample", "x": "x", "y": "y", "size": "particle_size",
            }],
        }
        normalized, _ = contract.normalize_request(bubble)
        self.assertEqual(normalized, bubble)
        self.assertFalse(node_supports_request(contract, bubble, current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"].update({
            "adapter_version": "1.1.0",
            "features": ["bubble"],
        })
        self.assertTrue(node_supports_request(contract, bubble, current_runtime))

        area = _request("line")
        area["operation"]["plot"]["type"] = "area"
        normalized, _ = contract.normalize_request(area)
        self.assertEqual(normalized, area)
        self.assertFalse(node_supports_request(contract, area, current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"].update({
            "adapter_version": "1.1.0",
            "features": ["area"],
        })
        self.assertTrue(node_supports_request(contract, area, current_runtime))

        stacked_line = _request("line")
        stacked_line["operation"]["plot"] = {
            "type": "stacked_line",
            "stack_gap_percent": 8,
            "series": [
                {"input": "sample", "x": "x", "y": "sample_a"},
                {"input": "sample", "x": "x", "y": "sample_b"},
            ],
        }
        normalized, _ = contract.normalize_request(stacked_line)
        self.assertEqual(normalized, stacked_line)
        self.assertFalse(node_supports_request(contract, stacked_line, current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"].update({
            "adapter_version": "1.2.0",
            "features": ["stacked_line"],
        })
        self.assertTrue(node_supports_request(contract, stacked_line, current_runtime))

        stacked_line["operation"]["plot"].pop("stack_gap_percent")
        with self.assertRaises(ValueError):
            contract.normalize_request(stacked_line)

        violin = _request("line")
        violin["operation"]["plot"] = {
            "type": "violin",
            "series": [
                {"input": "sample", "y": "strength"},
                {"input": "sample", "y": "modulus"},
            ],
        }
        normalized, _ = contract.normalize_request(violin)
        self.assertEqual(normalized, violin)
        self.assertFalse(node_supports_request(contract, violin, current_runtime))
        current_runtime["capability_runtime"]["origin.plot@v2"].update({
            "adapter_version": "1.3.0",
            "features": ["violin"],
        })
        self.assertTrue(node_supports_request(contract, violin, current_runtime))

        violin["operation"]["plot"]["series"][0]["style"] = {"color": "#3366CC"}
        with self.assertRaises(ValueError):
            contract.normalize_request(violin)

    def test_origin_contract_accepts_controlled_annotations_and_axis_options(self) -> None:
        contract = get_contract("origin.plot@v2")
        request = _request("line")
        request["operation"]["plot"].update({
            "x_axis": {"minor_ticks": 4, "reverse": True},
            "annotations": [
                {
                    "kind": "reference_line", "axis": "x", "value": 29.4,
                    "color": "#CC0000", "line_style": "dash", "label": "Peak",
                },
                {
                    "kind": "reference_band", "axis": "y", "from": 10, "to": 20,
                    "color": "#3366CC", "transparency": 80,
                },
                {
                    "kind": "text", "text": "Sample A", "x": 40, "y": 80,
                    "font_size": 10, "background": "white",
                },
            ],
        })
        normalized, _ = contract.normalize_request(request)
        self.assertEqual(normalized, request)

        request["operation"]["plot"]["type"] = "polar"
        with self.assertRaises(ValueError):
            contract.normalize_request(request)

    def test_semantic_version_comparison_is_numeric(self) -> None:
        self.assertTrue(version_at_least("0.10.0", "0.4.0"))
        self.assertFalse(version_at_least("0.3.9", "0.4.0"))

    def test_registry_hot_reloads_and_retains_last_valid_snapshot(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            root = Path(temporary)
            target = root / "origin.plot.v2.json"
            copy2(CONTRACT_ROOT / target.name, target)
            registry = _ContractRegistry(root)
            original = registry.contracts()["origin.plot@v2"]

            value = json.loads(target.read_text(encoding="utf-8"))
            value["display_name"] = "OriginPro hot reload"
            target.write_text(json.dumps(value), encoding="utf-8")
            refreshed = registry.contracts()["origin.plot@v2"]
            self.assertEqual(refreshed.display_name, "OriginPro hot reload")

            target.write_text("{invalid", encoding="utf-8")
            with self.assertLogs("core.software_contracts", level="ERROR"):
                retained = registry.contracts()["origin.plot@v2"]
            self.assertEqual(retained.display_name, "OriginPro hot reload")
            self.assertNotEqual(original.display_name, retained.display_name)


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