"""Convert stabilized Playwright test cases into a normalized relational schema."""

import dataclasses
import json
import uuid as _uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

from mcp.types import TextContent, Tool

from maeris_mcp.storage.execution_store import TestExecutionStore
from maeris_mcp.storage.test_store import TestGenerationStore
from maeris_mcp.types.test_generation import (
    ActionType,
    AssertionType,
    TestAssertion,
    TestCase,
    TestStep,
)
from maeris_mcp.types.test_structure import (
    ComponentRecord,
    FluRecord,
    FluTreeRecord,
    TestCaseRecord,
    TestCaseVerificationRecord,
    TestStructureOutput,
)


def convert_tests_to_structure_tool() -> Tool:
    """Return the tool definition for convert_tests_to_structure."""
    return Tool(
        name="convert_tests_to_structure",
        description=(
            "Converts generated (and optionally executed) Playwright test cases into a "
            "normalized relational schema with 5 tables: testcases, flus (functional user "
            "actions/steps), flu_tree (step relationships), components (UI elements), and "
            "testcases_verifications (assertions). "
            "When exec_session_id is provided, only converts tests that PASSED execution. "
            "When only session_id is provided, converts all generated tests. "
            "Writes test_structure.json to the output directory and returns the full structure."
        ),
        inputSchema={
            "type": "object",
            "required": ["session_id"],
            "properties": {
                "session_id": {
                    "type": "string",
                    "description": (
                        "Test generation session ID from generate_tests. "
                        "Used to load test case definitions."
                    ),
                },
                "exec_session_id": {
                    "type": "string",
                    "description": (
                        "Optional execution session ID from run_and_fix_tests. "
                        "When provided, only test cases that PASSED execution are converted. "
                        "When omitted, all generated test cases are converted."
                    ),
                },
                "output_dir": {
                    "type": "string",
                    "description": (
                        "Optional override for the output directory where test_structure.json "
                        "will be written. Defaults to the generation session's output_dir."
                    ),
                },
            },
        },
    )


# ── Classification constants ──────────────────────────────────────────────────

_HAPPY_KW = frozenset(
    {"happy", "success", "valid", "positive", "complete", "correct", "working", "smoke"}
)
_ERROR_KW = frozenset(
    {
        "error", "fail", "invalid", "negative", "empty", "missing", "wrong",
        "reject", "validation", "boundary", "edge", "bad",
    }
)
_NEGATIVE_ASSERTION_TYPES = frozenset(
    {
        AssertionType.HIDDEN,
        AssertionType.ELEMENT_NOT_EXISTS,
        AssertionType.API_NOT_CALLED,
        AssertionType.DISABLED,
        AssertionType.INPUT_EMPTY,
        AssertionType.ELEMENT_COUNT_LESS,
    }
)
_NEGATIVE_DESC_KW = frozenset({"not", "absent", "fail", "error", "prevent", "reject"})

# Only 4 valid action_taken values: "click" | "send_keys" | "go_to" | "hover"
_ACTION_TO_ACTION_TAKEN: dict[ActionType, str | None] = {
    ActionType.GO_TO: "go_to",
    ActionType.CLICK: "click",
    ActionType.SEND_KEYS: "send_keys",
    ActionType.SELECT: "send_keys",
    ActionType.HOVER: "hover",
    ActionType.PRESS: "send_keys",
    ActionType.UPLOAD: "send_keys",
    ActionType.SCREENSHOT: None,
}

_ACTION_TO_TAG: dict[ActionType, str | None] = {
    ActionType.GO_TO: "body",
    ActionType.CLICK: "button",
    ActionType.SEND_KEYS: "input",
    ActionType.SELECT: "select",
    ActionType.HOVER: "div",
    ActionType.PRESS: "input",
    ActionType.UPLOAD: "input",
    ActionType.SCREENSHOT: "body",
}


# ── Helpers ───────────────────────────────────────────────────────────────────

def _slugify(value: str) -> str:
    """Convert a string to a valid Python identifier (must match playwright_generator.py)."""
    cleaned = "".join(ch.lower() if ch.isalnum() or ch.isspace() else "_" for ch in value)
    cleaned = "_".join([p for p in cleaned.split() if p])
    return cleaned or "flow"


def _extract_path(steps: list[TestStep]) -> str:
    """Return the URL path from the first go_to step, or '/'."""
    for step in steps:
        if step.action == ActionType.GO_TO:
            url = step.mock_input or step.url
            if url:
                return urlparse(url).path or "/"
    return "/"


def _classify_test_type(tc: TestCase) -> str:
    """Classify a test case as 'happy_path' or 'error_path'."""
    text = (tc.name + " " + tc.description + " " + " ".join(tc.tags)).lower()

    happy_score = sum(1 for kw in _HAPPY_KW if kw in text)
    error_score = sum(1 for kw in _ERROR_KW if kw in text)

    # Tag bonus (stronger signal)
    if any(t in ("happy_path", "smoke", "positive") for t in tc.tags):
        happy_score += 2
    if any(t in ("negative", "error_path", "validation", "edge_case") for t in tc.tags):
        error_score += 2

    # Any negative assertion type weights toward error_path
    if any(a.type in _NEGATIVE_ASSERTION_TYPES for a in tc.assertions):
        error_score += 1

    if happy_score > error_score:
        return "happy_path"
    if error_score > happy_score:
        return "error_path"
    # Tie-break by priority
    return "happy_path" if tc.priority in ("critical", "high") else "error_path"


def _infer_tag(step: TestStep) -> str | None:
    """Infer HTML tag from action type and selector."""
    base = _ACTION_TO_TAG.get(step.action)
    if step.action == ActionType.CLICK and step.selector:
        sel = step.selector.lower()
        if any(x in sel for x in ["a[", "href", "link"]):
            return "a"
        if any(x in sel for x in ["[role=button]", "btn-", ".btn", "button"]):
            return "button"
    return base


def _infer_validation_type(step: TestStep, tag: str | None) -> str | None:
    """Infer validation_type for input/select elements."""
    if tag == "select":
        return "select"
    if tag == "input":
        if step.action == ActionType.UPLOAD:
            return "file"
        sel = (step.selector or "").lower()
        for keyword, vtype in [
            ("email", "email"), ("password", "password"),
            ("number", "number"), ("qty", "number"), ("quantity", "number"),
            ("tel", "tel"), ("phone", "tel"), ("date", "date"),
        ]:
            if keyword in sel:
                return vtype
        return "text"
    return None


def _infer_html(step: TestStep, tag: str | None, vtype: str | None) -> str | None:
    """Build a concise HTML representation of the component."""
    if not tag:
        return None
    desc = (step.description or "")[:40]
    if tag == "input":
        t = vtype or "text"
        if t in ("checkbox", "file"):
            return f'<input type="{t}">'
        return f'<input type="{t}" placeholder="{desc}">'
    if tag == "select":
        return f'<select>{desc}</select>'
    if tag == "button":
        return f'<button>{desc}</button>'
    if tag == "a":
        return f'<a href="#">{desc}</a>'
    if tag == "body":
        return "<body>"
    return f'<{tag}>{desc}</{tag}>'


def _classify_assertion_nature(assertion: TestAssertion) -> str:
    """Return 'positive' or 'negative' for an assertion."""
    nature = "negative" if assertion.type in _NEGATIVE_ASSERTION_TYPES else "positive"
    # Description text can flip positive → negative
    if nature == "positive" and assertion.description:
        desc_lower = assertion.description.lower()
        if any(kw in desc_lower for kw in _NEGATIVE_DESC_KW):
            nature = "negative"
    return nature


def _build_custom_input_data(components: list[ComponentRecord]) -> dict[str, Any]:
    """Build custom_input_data with rows mapping component_id → mock_input."""
    rows = {
        comp.component_id: comp.mock_input
        for comp in components
        if comp.mock_input is not None
    }
    return {"rows": rows} if rows else {}


def _build_flus(testcase_id: str, steps: list[TestStep]) -> list[FluRecord]:
    """Build FluRecords from all steps. Each flu_id is a UUID."""
    records = []
    step_index = 0
    for step in steps:
        records.append(FluRecord(
            flu_id=str(_uuid.uuid4()),
            testcase_id=testcase_id,
            action=step.action.value,
            description=step.description,
            selector=step.selector,
            selector_type=step.selector_type.value if step.selector_type else None,
            value=step.value,
            url=step.url,
            timeout=step.timeout,
            condition=step.condition,
            step_index=step_index,
        ))
        step_index += 1
    return records


def _build_flu_tree(testcase_id: str, flu_ids: list[str]) -> list[FluTreeRecord]:
    """Build sequential parent→child relationships between flus."""
    return [
        FluTreeRecord(
            tree_id=str(_uuid.uuid4()),
            testcase_id=testcase_id,
            parent_flu_id=flu_ids[i],
            child_flu_id=flu_ids[i + 1],
            relationship="sequential",
        )
        for i in range(len(flu_ids) - 1)
    ]


def _build_components(
    testcase_id: str, flus: list[FluRecord], steps: list[TestStep]
) -> list[ComponentRecord]:
    """Build ComponentRecords, one per flu step. Tracks page_url."""
    components = []
    current_page_url: str | None = None
    flu_iter = iter(flus)

    for step in steps:
        flu = next(flu_iter)

        # Track current page URL from go_to steps
        if step.action == ActionType.GO_TO:
            current_page_url = step.mock_input or step.url

        tag = _infer_tag(step)
        vtype = _infer_validation_type(step, tag)
        html_repr = _infer_html(step, tag, vtype)
        action_taken = _ACTION_TO_ACTION_TAKEN.get(step.action)

        if step.action == ActionType.GO_TO:
            nav_url = step.mock_input or step.url
            mock_input = nav_url
            css_selector = nav_url or ""
        elif step.action in (ActionType.SEND_KEYS, ActionType.PRESS):
            mock_input = step.value
            css_selector = step.selector or ""
        else:
            mock_input = None
            css_selector = step.selector or ""

        desc = step.description or step.selector or ""
        comp_repr = f"{tag} — {desc[:60]}" if tag else (desc[:60] or None)

        components.append(ComponentRecord(
            component_id=str(_uuid.uuid4()),
            flu_id=flu.flu_id,
            testcase_id=testcase_id,
            css_selector=css_selector,  # mandatory, never null
            nth_appearance=1,
            page_url=current_page_url,
            action_taken=action_taken,
            html_of_component=html_repr,
            tag=tag,
            validation_type=vtype,
            mock_input=mock_input,
            component_representation_string=comp_repr or None,
        ))

    return components


def _build_verifications(
    testcase_id: str, tc_index: int, assertions: list[TestAssertion]
) -> list[TestCaseVerificationRecord]:
    return [
        TestCaseVerificationRecord(
            verification_id=f"ver_{tc_index}_{i}",
            testcase_id=testcase_id,
            assertion_type=a.type.value,
            description=a.description,
            selector=a.selector,
            expected=a.expected,
            assertion_nature=_classify_assertion_nature(a),
            timeout=a.timeout,
            attribute=a.attribute,
            api_endpoint=a.api_endpoint,
            assertion_index=i,
        )
        for i, a in enumerate(assertions)
    ]


def _write_output(output: TestStructureOutput, output_dir: str) -> str:
    """Serialize output to test_structure.json and return the file path."""
    path = Path(output_dir) / "test_structure.json"
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        data = dataclasses.asdict(output)
        path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
    except Exception as e:
        raise RuntimeError(f"Could not write test_structure.json: {e}") from e
    return str(path)


# ── Handler ───────────────────────────────────────────────────────────────────

async def handle_convert_tests_to_structure(
    test_store: TestGenerationStore,
    exec_store: TestExecutionStore,
    arguments: dict,
) -> list[TextContent]:
    """Handle the convert_tests_to_structure tool call."""
    session_id = arguments.get("session_id") or arguments.get("sessionId")
    exec_session_id = arguments.get("exec_session_id") or arguments.get("execSessionId")
    output_dir_override = arguments.get("output_dir") or arguments.get("outputDir")

    # ── Validate generation session ───────────────────────────────────────────
    if not session_id:
        return [TextContent(type="text", text="Error: session_id is required.")]

    gen_session = test_store.get_session(session_id)
    if not gen_session:
        return [TextContent(type="text", text=f"Error: generation session not found: {session_id}")]

    if not gen_session.output_dir and not output_dir_override:
        return [TextContent(
            type="text",
            text=(
                "Error: generation session has no output directory. "
                "Has generate_tests been finalized (is_last=true)?"
            ),
        )]

    if not gen_session.test_cases:
        return [TextContent(type="text", text="Error: generation session has no test cases.")]

    # ── Resolve output directory ──────────────────────────────────────────────
    output_dir = output_dir_override or gen_session.output_dir
    assert output_dir  # guaranteed by checks above

    # ── Resolve execution session (optional) ──────────────────────────────────
    exec_session = None
    passed_files: set[str] = set()
    status_map: dict[str, str] = {}

    if exec_session_id:
        exec_session = exec_store.get_session(exec_session_id)
        if not exec_session:
            return [TextContent(
                type="text",
                text=f"Error: execution session not found: {exec_session_id}",
            )]
        passed_files = {r.test_file for r in exec_session.results if r.status == "passed"}
        status_map = {r.test_file: r.status for r in exec_session.results}
        # Use exec session output_dir if more specific
        if exec_session.output_dir and not output_dir_override:
            output_dir = exec_session.output_dir

    filter_mode = "passed_only" if exec_session_id else "all"

    # ── Convert each test case ────────────────────────────────────────────────
    session_slug = session_id[:8]
    all_testcases: list[TestCaseRecord] = []
    all_flus: list[FluRecord] = []
    all_flu_tree: list[FluTreeRecord] = []
    all_components: list[ComponentRecord] = []
    all_verifications: list[TestCaseVerificationRecord] = []
    parse_errors = 0

    for original_idx, raw_tc in enumerate(gen_session.test_cases):
        test_file = _slugify(raw_tc.get("name", f"test_{original_idx}")) + ".py"

        # Filter: skip tests that didn't pass (when exec session provided)
        if exec_session_id and test_file not in passed_files:
            continue

        # Parse into typed model
        try:
            tc = TestCase.model_validate(raw_tc)
        except Exception:
            parse_errors += 1
            continue

        # Assign stable IDs
        tc_index = len(all_testcases)  # 0-based position in output (not original index)
        testcase_id = f"tc_{session_slug}_{tc_index}"

        # Build flus (UUIDs)
        flus = _build_flus(testcase_id, tc.steps)
        all_flus.extend(flus)

        # Build flu_tree (linear chain of UUIDs)
        flu_ids = [f.flu_id for f in flus]
        all_flu_tree.extend(_build_flu_tree(testcase_id, flu_ids))

        # Build components (new schema, one per step)
        components = _build_components(testcase_id, flus, tc.steps)
        all_components.extend(components)

        # Build testcase record AFTER components (needs component UUIDs for custom_input_data)
        all_testcases.append(
            TestCaseRecord(
                testcase_id=testcase_id,
                session_id=session_id,
                name=tc.name,
                description=tc.description,
                path=_extract_path(tc.steps),
                test_type=_classify_test_type(tc),
                priority=tc.priority,
                tags=list(tc.tags),
                custom_input_data=_build_custom_input_data(components),
                execution_status=status_map.get(test_file),
                test_file=test_file,
            )
        )

        # Build verifications
        all_verifications.extend(_build_verifications(testcase_id, tc_index, tc.assertions))

    # ── Warn if nothing survived the filter ───────────────────────────────────
    if not all_testcases and exec_session_id:
        response = {
            "warning": (
                "No tests passed execution — the structure output is empty. "
                "Run run_and_fix_tests until at least one test passes."
            ),
            "filter_mode": filter_mode,
            "testcases": [],
            "flus": [],
            "flu_tree": [],
            "components": [],
            "testcases_verifications": [],
            "summary": {
                "testcases": 0,
                "flus": 0,
                "flu_tree": 0,
                "components": 0,
                "testcases_verifications": 0,
                "parse_errors": parse_errors,
            },
        }
        return [TextContent(type="text", text=json.dumps(response, indent=2))]

    # ── Build output ──────────────────────────────────────────────────────────
    summary = {
        "testcases": len(all_testcases),
        "flus": len(all_flus),
        "flu_tree": len(all_flu_tree),
        "components": len(all_components),
        "testcases_verifications": len(all_verifications),
        "parse_errors": parse_errors,
    }

    output = TestStructureOutput(
        session_id=session_id,
        exec_session_id=exec_session_id,
        flow_description=gen_session.flow_description,
        generated_at=datetime.now(timezone.utc).isoformat(),
        filter_mode=filter_mode,
        testcases=all_testcases,
        flus=all_flus,
        flu_tree=all_flu_tree,
        components=all_components,
        testcases_verifications=all_verifications,
        summary=summary,
    )

    try:
        output_file = _write_output(output, output_dir)
    except RuntimeError as e:
        return [TextContent(type="text", text=f"Error: {e}")]

    output.output_file = output_file

    response = dataclasses.asdict(output)
    return [TextContent(type="text", text=json.dumps(response, indent=2, default=str))]
