from __future__ import annotations

import json
import tempfile
import unittest
import zipfile
from io import BytesIO
from pathlib import Path
from unittest.mock import patch

from evaluation.audit import (
    _subprocess_env,
    audit_repository,
    audit_security_repository,
)
from evaluation.client import EvalClientError, require_safe_base_url
from evaluation.combine import combine_reports
from evaluation.inspect_bridge import export_inspect_jsonl
from evaluation.models import AssertionSpec, EvalConfigError, RunObservation
from evaluation.report import render_markdown, summarize
from evaluation.scoring import score_observation
from evaluation.suite import load_suite


class EvaluationSuiteTests(unittest.TestCase):
    def test_smoke_suite_valid(self):
        root = Path(__file__).resolve().parents[1]
        suite = load_suite(root / "evaluation" / "datasets" / "smoke.json")
        self.assertEqual(suite.name, "zcbot-smoke")
        self.assertGreaterEqual(len(suite.cases), 4)
        production = load_suite(
            root / "evaluation" / "datasets" / "production_smoke.json"
        )
        self.assertEqual(production.default_repetitions, 1)
        self.assertNotIn(
            "security", {case.dimension for case in production.cases}
        )
        materials = load_suite(
            root / "evaluation" / "datasets" / "materials_core.json"
        )
        self.assertEqual(len(materials.cases), 10)
        full_safe = load_suite(
            root / "evaluation" / "datasets" / "production_full_safe.json"
        )
        self.assertEqual(len(full_safe.cases), 3)
        self.assertEqual(
            {case.dimension for case in full_safe.cases},
            {"task_quality", "reliability", "performance"},
        )

    def test_inspect_export(self):
        root = Path(__file__).resolve().parents[1]
        suite = load_suite(root / "evaluation" / "datasets" / "smoke.json")
        with tempfile.TemporaryDirectory() as tmp:
            output = Path(tmp) / "inspect.jsonl"
            count = export_inspect_jsonl(suite, output)
            records = [
                json.loads(line)
                for line in output.read_text(encoding="utf-8").splitlines()
            ]
        self.assertEqual(count, len(suite.cases))
        self.assertEqual(records[0]["id"], suite.cases[0].id)
        self.assertIn("assertions", records[0]["metadata"])

    def test_duplicate_case_id_rejected(self):
        raw = {
            "cases": [
                {
                    "id": "same",
                    "dimension": "task_quality",
                    "prompt": "a",
                    "assertions": [{"type": "response_nonempty"}],
                },
                {
                    "id": "same",
                    "dimension": "task_quality",
                    "prompt": "b",
                    "assertions": [{"type": "response_nonempty"}],
                },
            ]
        }
        with tempfile.TemporaryDirectory() as tmp:
            path = Path(tmp) / "suite.json"
            path.write_text(json.dumps(raw), encoding="utf-8")
            with self.assertRaises(EvalConfigError):
                load_suite(path)


class ScoringTests(unittest.TestCase):
    def test_weighted_response_and_artifact_score(self):
        observation = RunObservation(
            response="CaCO3",
            run_status="idle",
            artifact_loader=lambda path: b"ok" if path == "a.txt" else b"",
        )
        specs = (
            AssertionSpec("run_succeeded", weight=2),
            AssertionSpec("response_contains", value="CaCO3", weight=2),
            AssertionSpec("response_contains", value="wrong", weight=1),
            AssertionSpec("artifact_exists", path="a.txt", weight=1),
        )
        score, results = score_observation(specs, observation)
        self.assertAlmostEqual(score, 5 / 6)
        self.assertEqual(sum(r.passed for r in results), 3)

    def test_zip_validation(self):
        stream = BytesIO()
        with zipfile.ZipFile(stream, "w") as archive:
            archive.writestr("x.txt", "ok")
        observation = RunObservation(
            artifact_loader=lambda _path: stream.getvalue()
        )
        score, _ = score_observation(
            (AssertionSpec("artifact_zip_valid", path="x.pptx"),),
            observation,
        )
        self.assertEqual(score, 1.0)


class ClientSafetyTests(unittest.TestCase):
    def test_local_url_allowed(self):
        self.assertEqual(
            require_safe_base_url(
                "http://127.0.0.1:8765", allow_remote=False
            ),
            "http://127.0.0.1:8765/",
        )

    def test_remote_url_requires_explicit_opt_in(self):
        with self.assertRaises(EvalClientError):
            require_safe_base_url(
                "https://zcbot.example.com", allow_remote=False
            )
        self.assertEqual(
            require_safe_base_url(
                "https://zcbot.example.com", allow_remote=True
            ),
            "https://zcbot.example.com/",
        )


class ReportTests(unittest.TestCase):
    def test_direct_audit_environment_ignores_proxy(self):
        for key in (
            "HTTP_PROXY",
            "HTTPS_PROXY",
            "ALL_PROXY",
            "http_proxy",
            "https_proxy",
            "all_proxy",
        ):
            with self.subTest(key=key), patch.dict(
                "evaluation.audit.os.environ",
                {key: "http://proxy.example"},
                clear=True,
            ):
                direct_env = _subprocess_env(direct_network=True)
                inherited_env = _subprocess_env(direct_network=False)

            direct_keys = {item.casefold() for item in direct_env}
            inherited_by_key = {
                item.casefold(): value for item, value in inherited_env.items()
            }
            self.assertNotIn(key.casefold(), direct_keys)
            self.assertEqual(
                inherited_by_key[key.casefold()], "http://proxy.example"
            )
            self.assertEqual(direct_env["PYTHONUTF8"], "1")

    def test_combine_existing_dimension_reports(self):
        engineering = {
            "suite": {"name": "engineering", "version": "1"},
            "generated_at": "2026-01-01T00:00:00+00:00",
            "environment": {"git_commit": "abc"},
            "dimensions": {
                "engineering": {
                    "weight": 15,
                    "score": 40,
                    "case_count": 1,
                }
            },
            "security_cap": {"failures": []},
            "cases": [],
        }
        smoke = {
            "suite": {"name": "smoke", "version": "1"},
            "generated_at": "2026-01-02T00:00:00+00:00",
            "environment": {"git_commit": "abc"},
            "dimensions": {
                "task_quality": {
                    "weight": 40,
                    "score": 100,
                    "case_count": 1,
                },
                "performance": {
                    "weight": 10,
                    "score": 100,
                    "case_count": 1,
                },
            },
            "security_cap": {"failures": []},
            "cases": [],
        }
        with tempfile.TemporaryDirectory() as tmp:
            engineering_path = Path(tmp) / "engineering.json"
            smoke_path = Path(tmp) / "smoke.json"
            engineering_path.write_text(
                json.dumps(engineering), encoding="utf-8"
            )
            smoke_path.write_text(json.dumps(smoke), encoding="utf-8")
            combined = combine_reports([engineering_path, smoke_path])
        self.assertEqual(combined["provisional_score"], 86.15)
        self.assertIsNone(combined["total_score"])
        self.assertFalse(combined["is_complete"])
        self.assertEqual(len(combined["sources"]), 2)

    def test_incomplete_dimensions_have_no_total(self):
        root = Path(__file__).resolve().parents[1]
        suite = load_suite(root / "evaluation" / "datasets" / "smoke.json")
        summary = summarize(suite, [], repo_root=root)
        self.assertIsNone(summary["total_score"])
        self.assertFalse(summary["is_complete"])
        self.assertIn("总分为 N/A", render_markdown(summary))

    def test_audit_result_has_engineering_dimension(self):
        # 用极短超时验证固定评分结构，不要求当前仓库全量命令在此测试内跑完。
        root = Path(__file__).resolve().parents[1]
        result = audit_repository(
            root, timeout_s=0.001, write_evidence=False
        )
        self.assertEqual(result.case.dimension, "engineering")
        types = {item.type for item in result.repetitions[0].assertions}
        self.assertIn("unit_tests", types)
        self.assertIn("coverage", types)
        self.assertNotIn("pip-audit", types)
        security = audit_security_repository(
            root, timeout_s=0.001, write_evidence=False
        )
        self.assertEqual(security.case.dimension, "security")
        security_types = {
            item.type for item in security.repetitions[0].assertions
        }
        self.assertIn("bandit", security_types)
        self.assertIn("pip-audit", security_types)


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