"""Dynamics 365 solution smoke test helpers.

Smoke assertions use pac CLI to probe the target environment
after a solution import. All assertions run via subprocess.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List
from pathlib import Path
import re
import xml.etree.ElementTree as ET

from g_gremlin.dynamics import PacWrapper


@dataclass
class SmokeAssertion:
    type: str
    params: Dict[str, Any]


@dataclass
class SmokeResult:
    assertion: SmokeAssertion
    passed: bool
    detail: str = ""

    @property
    def skipped(self) -> bool:
        return self.detail.startswith("skipped:")

    def to_dict(self) -> Dict[str, Any]:
        return {
            "type": self.assertion.type,
            "params": self.assertion.params,
            "passed": self.passed,
            "detail": self.detail,
        }


@dataclass
class SmokeReport:
    results: List[SmokeResult]

    @property
    def all_passed(self) -> bool:
        return all(r.passed or r.skipped for r in self.results)

    @property
    def summary(self) -> Dict[str, int]:
        total = len(self.results)
        passed = len([r for r in self.results if r.passed])
        failed = len([r for r in self.results if (not r.passed) and (not r.skipped)])
        skipped = len([r for r in self.results if r.skipped])
        return {"total": total, "passed": passed, "failed": failed, "skipped": skipped}

    def to_dict(self) -> Dict[str, Any]:
        return {
            "summary": self.summary,
            "results": [r.to_dict() for r in self.results],
        }


def load_smoke_assertions(pack_yaml_dict: Dict[str, Any]) -> List[SmokeAssertion]:
    items = pack_yaml_dict.get("smoke") or []
    assertions: List[SmokeAssertion] = []
    if isinstance(items, list):
        for item in items:
            if not isinstance(item, dict):
                continue
            atype = item.get("type") or item.get("assertion")
            params = item.get("params") or {k: v for k, v in item.items() if k not in {"type", "assertion"}}
            if atype:
                assertions.append(SmokeAssertion(type=str(atype), params=params))
    return assertions


def run_smoke(
    pac: PacWrapper,
    assertions: List[SmokeAssertion],
    *,
    solution_folder: Path | None = None,
) -> SmokeReport:
    results: List[SmokeResult] = []
    solutions_cache: List[Dict[str, Any]] | None = None

    for assertion in assertions:
        atype = assertion.type
        params = assertion.params or {}
        if atype == "solution_exists":
            solutions_cache = solutions_cache or pac.solution_list()
            name = str(params.get("solution_name") or params.get("name") or "")
            passed = _solution_exists(solutions_cache, name)
            detail = "" if passed else f"solution not found: {name}"
            results.append(SmokeResult(assertion=assertion, passed=passed, detail=detail))
            continue

        if atype == "solution_version_gte":
            solutions_cache = solutions_cache or pac.solution_list()
            name = str(params.get("solution_name") or params.get("name") or "")
            min_version = str(params.get("min_version") or "")
            found = _find_solution(solutions_cache, name)
            if not found:
                results.append(SmokeResult(assertion=assertion, passed=False, detail=f"solution not found: {name}"))
                continue
            version = str(found.get("version") or "")
            passed = _version_gte(version, min_version)
            detail = "" if passed else f"version {version} < {min_version}"
            results.append(SmokeResult(assertion=assertion, passed=passed, detail=detail))
            continue

        if atype == "table_exists":
            logical_name = str(params.get("logical_name") or "")
            if not solution_folder:
                results.append(
                    SmokeResult(assertion=assertion, passed=False, detail="skipped: no solution folder")
                )
                continue
            exists = (solution_folder / "Entities" / logical_name / "Entity.xml").exists()
            results.append(
                SmokeResult(
                    assertion=assertion,
                    passed=exists,
                    detail="" if exists else f"table not found: {logical_name}",
                )
            )
            continue

        if atype == "web_resource_exists":
            name = str(params.get("name") or "")
            if not solution_folder:
                results.append(
                    SmokeResult(assertion=assertion, passed=False, detail="skipped: no solution folder")
                )
                continue
            exists = _web_resource_exists(solution_folder, name)
            results.append(
                SmokeResult(
                    assertion=assertion,
                    passed=exists,
                    detail="" if exists else f"web resource not found: {name}",
                )
            )
            continue

        if atype == "workflow_active":
            name = str(params.get("name") or "")
            if not solution_folder:
                results.append(
                    SmokeResult(assertion=assertion, passed=False, detail="skipped: no solution folder")
                )
                continue
            active = _workflow_active(solution_folder, name)
            results.append(
                SmokeResult(
                    assertion=assertion,
                    passed=active,
                    detail="" if active else f"workflow not active: {name}",
                )
            )
            continue

        raise ValueError(f"Unknown smoke assertion type: {atype}")

    return SmokeReport(results=results)


# ---- internal helpers ----

def _find_solution(solutions: List[Dict[str, Any]], name: str) -> Dict[str, Any] | None:
    for item in solutions:
        unique = str(item.get("unique_name") or item.get("UniqueName") or item.get("name") or "")
        if unique.lower() == name.lower():
            return item
        friendly = str(item.get("friendly_name") or item.get("FriendlyName") or item.get("displayName") or "")
        if friendly.lower() == name.lower():
            return item
    return None


def _solution_exists(solutions: List[Dict[str, Any]], name: str) -> bool:
    if not name:
        return False
    return _find_solution(solutions, name) is not None


def _version_gte(version: str, minimum: str) -> bool:
    if not version or not minimum:
        return False
    a = [int(p) for p in re.split(r"\D+", version) if p]
    b = [int(p) for p in re.split(r"\D+", minimum) if p]
    max_len = max(len(a), len(b))
    a += [0] * (max_len - len(a))
    b += [0] * (max_len - len(b))
    return a >= b


def _web_resource_exists(solution_folder: Path, name: str) -> bool:
    if not name:
        return False
    root = solution_folder / "WebResources"
    if not root.exists():
        return False
    for path in root.rglob("*"):
        if not path.is_file():
            continue
        if path.name == name or path.as_posix().endswith(name):
            return True
    return False


def _workflow_active(solution_folder: Path, name: str) -> bool:
    if not name:
        return False
    workflows = solution_folder / "Workflows"
    if not workflows.exists():
        return False
    for path in workflows.rglob("*.xml"):
        if path.stem.lower() != name.lower():
            continue
        try:
            root = ET.fromstring(path.read_text(encoding="utf-8"))
        except Exception:
            continue
        for elem in root.iter():
            tag = elem.tag.split("}")[-1].lower()
            if tag == "statecode" and elem.text and elem.text.strip() == "1":
                return True
    return False
