"""Import Playwright, Cypress, or Selenium tests into Maeris manual testcases."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import re
from typing import Iterable

from maeris_mcp.types.test_generation import ActionType


ALLOWED_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".py", ".java", ".cs"}

ACTION_TO_MANUAL = {
    ActionType.GO_TO: "go_to",
    ActionType.CLICK: "click",
    ActionType.SEND_KEYS: "send_keys",
    ActionType.HOVER: "hover",
    ActionType.UPLOAD: "upload",
}


@dataclass
class ImportedStep:
    action: ActionType
    description: str
    selector: str | None = None
    value: str | None = None
    url: str | None = None


@dataclass
class ImportedTestCase:
    name: str
    steps: list[ImportedStep]
    assertions: list["ImportedAssertion"]
    framework: str
    source_path: str


@dataclass
class ImportedAssertion:
    nature: str
    atype: str
    value: str | None = None
    selector: str | None = None
    description: str | None = None


def _first_string_literal(value: str) -> str | None:
    match = re.search(r"(['\"`])((?:\\.|(?!\1).)*)\1", value)
    if not match:
        return None
    return match.group(2)


def _extract_python_string_constants(content: str) -> dict[str, str]:
    """Extract module-level string constant assignments, e.g. EMAIL = 'foo@bar.com'."""
    constants: dict[str, str] = {}
    for m in re.finditer(
        r"^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(['\"`])((?:\\.|(?!\2).)*)\2",
        content,
        re.M,
    ):
        constants[m.group(1)] = m.group(3)
    return constants


def _resolve_str_arg(arg: str, constants: dict[str, str]) -> str | None:
    """Return the string value of arg: first try as a literal, then as a constant name."""
    lit = _first_string_literal(arg)
    if lit is not None:
        return lit
    return constants.get(arg.strip())


def _human_value_from_selector(selector: str) -> str:
    """Convert a Playwright selector to a human-readable assertion value.

    Strips the ``text=`` prefix and any surrounding quotes so that
    ``text="return on investment"`` becomes ``return on investment``
    and ``text=Estimate Your Savings`` becomes ``Estimate Your Savings``.
    Non-text selectors are returned unchanged.
    """
    if not selector.startswith("text="):
        return selector
    text = selector[5:]  # drop "text="
    # Strip optional surrounding quotes used for Playwright exact-match syntax
    if len(text) >= 2 and text[0] in ('"', "'") and text[-1] == text[0]:
        text = text[1:-1]
    return text


def _extract_base_url(content: str) -> str | None:
    match = re.search(r"^\s*BASE_URL\s*=\s*(['\"])(?P<url>.*?)\1\s*$", content, re.M)
    if not match:
        return None
    return match.group("url").strip() or None


def _resolve_base_url_literal(url: str | None, base_url: str | None) -> str | None:
    if not url or not base_url:
        return url
    resolved = url.replace("{BASE_URL}", base_url).replace("${BASE_URL}", base_url)
    return resolved


def _normalize_url(url: str | None, base_url: str | None) -> str | None:
    if not url:
        return None
    if base_url and url.startswith("/"):
        return base_url.rstrip("/") + url
    return url


def _collect_paths(paths: Iterable[str]) -> list[Path]:
    collected: list[Path] = []
    for raw in paths:
        p = Path(raw).expanduser()
        if p.is_file():
            collected.append(p)
            continue
        if p.is_dir():
            for file in p.rglob("*"):
                if file.is_file() and file.suffix.lower() in ALLOWED_EXTENSIONS:
                    collected.append(file)
            continue
    return collected


def _probable_test_files(files: list[Path]) -> list[Path]:
    if not files:
        return []
    filtered: list[Path] = []
    for file in files:
        name = file.name.lower()
        parts = {p.lower() for p in file.parts}
        if any(token in name for token in ("test", "spec", "e2e")):
            filtered.append(file)
            continue
        if parts & {"tests", "test", "e2e", "spec"}:
            filtered.append(file)
            continue
    return filtered or files


def discover_test_files(paths: Iterable[str]) -> list[Path]:
    # Prefer broad collection so AI fallback can analyze any candidate file.
    return _collect_paths(paths)


def _extract_js_tests(content: str) -> list[tuple[str, str]]:
    tests: list[tuple[str, str]] = []
    pattern = re.compile(r"\b(?:test|it)\s*\(\s*(['\"])(?P<name>.*?)\1\s*,", re.S)
    for match in pattern.finditer(content):
        name = match.group("name").strip() or "Test"
        cursor = match.end()
        # For arrow functions `async ({ page }) => { ... }`, the first `{` after
        # the cursor may be inside parameter destructuring (e.g. `({ page })`).
        # Prefer the `{` that immediately follows a `=>` arrow operator.
        segment = content[cursor:]
        arrow_match = re.search(r"=>\s*\{", segment)
        if arrow_match:
            brace_start = cursor + arrow_match.end() - 1  # position of `{`
        else:
            brace_start = content.find("{", cursor)
        if brace_start == -1:
            continue
        depth = 0
        end = None
        for i in range(brace_start, len(content)):
            char = content[i]
            if char == "{":
                depth += 1
            elif char == "}":
                depth -= 1
                if depth == 0:
                    end = i
                    break
        if end is None:
            continue
        body = content[brace_start + 1 : end]
        tests.append((name, body))
    return tests


def _normalize_chained_lines(lines: list[str]) -> list[str]:
    """Join chained calls split across lines (e.g., `await page` + `.click()`)."""
    merged: list[str] = []
    for raw in lines:
        line = raw.strip()
        if not line:
            continue
        if line.startswith(".") and merged:
            merged[-1] = merged[-1].rstrip() + line
            continue
        merged.append(line)
    return merged


def _extract_python_tests(content: str) -> list[tuple[str, str]]:
    tests: list[tuple[str, str]] = []
    lines = content.splitlines()

    # Extract module docstring first line — used as test name for def run() files
    module_doc_name: str | None = None
    stripped_content = content.lstrip()
    for _q in ('"""', "'''"):
        if stripped_content.startswith(_q):
            _end = stripped_content.find(_q, 3)
            if _end != -1:
                _first = stripped_content[3:_end].strip().splitlines()
                if _first and _first[0].strip():
                    module_doc_name = _first[0].strip()
            break

    i = 0
    while i < len(lines):
        line = lines[i]
        match = re.match(r"^(\s*)def\s+(test_[A-Za-z0-9_]+|run)\s*\(", line)
        if not match:
            i += 1
            continue
        indent = len(match.group(1))
        raw_name = match.group(2)
        # For def run(), use the module docstring first line as the test name
        name = module_doc_name if (raw_name == "run" and module_doc_name) else raw_name
        body_lines: list[str] = []
        i += 1
        while i < len(lines):
            next_line = lines[i]
            if next_line.strip() == "":
                body_lines.append(next_line)
                i += 1
                continue
            next_indent = len(next_line) - len(next_line.lstrip())
            if next_indent <= indent:
                break
            body_lines.append(next_line)
            i += 1
        tests.append((name, "\n".join(body_lines)))
    return tests


_SELENIUM_SIGNALS = re.compile(
    r"\b(?:from\s+selenium|import\s+selenium|webdriver\.|driver\.get\(|driver\.find_element)",
    re.M,
)
_PLAYWRIGHT_SCRIPT_SIGNALS = re.compile(
    r"\b(?:from\s+playwright|sync_playwright|page\.goto\(|page\.fill\(|page\.click\()",
    re.M,
)


def _extract_script_tests(content: str, file_stem: str) -> list[tuple[str, str]]:
    """Fallback: treat an entire script-style file as one test case.

    Used when no ``def test_*()`` or ``def run()`` functions are found but the
    file clearly contains Selenium or Playwright automation code.
    """
    if not (_SELENIUM_SIGNALS.search(content) or _PLAYWRIGHT_SCRIPT_SIGNALS.search(content)):
        return []
    name = file_stem.replace("_", " ").replace("-", " ").strip() or "Test"
    return [(name, content)]


def _count_python_tests(content: str) -> int:
    return len(re.findall(r"^\s*def\s+test_[A-Za-z0-9_]+\s*\(", content, re.M))


def _count_js_tests(content: str) -> int:
    return len(re.findall(r"\\b(?:test|it)\\s*\\(\\s*(['\\\"]).*?\\1\\s*,", content, re.S))


def _parse_playwright_js(body: str) -> list[ImportedStep]:
    steps: list[ImportedStep] = []
    for line in _normalize_chained_lines(body.splitlines()):
        line = line.strip()
        if not line:
            continue

        if ".goto(" in line:
            args = line.split(".goto(", 1)[1]
            url = _first_string_literal(args)
            if url:
                steps.append(ImportedStep(ActionType.GO_TO, "go_to", url=_normalize_url(url, None)))
            continue

        chain = re.search(
            r"\bpage\.(?:locator|getByRole|getByText|getByLabel|getByPlaceholder|getByTestId|getByAltText|getByTitle)\(([^)]*)\)"
            r"(?:\.\w+\([^)]*\))*"
            r"\.(click|fill|type|press|hover)\(([^)]*)\)",
            line,
        )
        if chain:
            selector = _first_string_literal(chain.group(1)) or chain.group(1).strip()
            action = chain.group(2)
            value = _first_string_literal(chain.group(3)) if action in ("fill", "type", "press") else None
            steps.extend(_action_from_js(action, selector, value))
            continue

        call = re.search(r"\bpage\.(click|fill|type|press|hover|selectOption)\((.*)\)", line)
        if call:
            action = call.group(1)
            args = call.group(2)
            parts = [p.strip() for p in args.split(",")]
            selector = _first_string_literal(parts[0]) if parts else None
            value = _first_string_literal(parts[1]) if len(parts) > 1 else None
            steps.extend(_action_from_js(action, selector, value))
            continue

        upload = re.search(r"\bpage\.setInputFiles\((.*)\)", line)
        if upload:
            parts = [p.strip() for p in upload.group(1).split(",")]
            selector = _first_string_literal(parts[0]) if parts else None
            value = _first_string_literal(parts[1]) if len(parts) > 1 else None
            steps.append(ImportedStep(ActionType.UPLOAD, "Upload file", selector=selector, value=value))
            continue

        if ".screenshot(" in line:
            steps.append(ImportedStep(ActionType.SCREENSHOT, "Screenshot"))

    return steps


def _parse_cypress_js(body: str) -> list[ImportedStep]:
    steps: list[ImportedStep] = []
    for line in _normalize_chained_lines(body.splitlines()):
        line = line.strip()
        if not line:
            continue

        visit = re.search(r"\bcy\.visit\((.*)\)", line)
        if visit:
            url = _first_string_literal(visit.group(1))
            if url:
                steps.append(ImportedStep(ActionType.GO_TO, "go_to", url=_normalize_url(url, None)))
            continue

        chain = re.search(
            r"\bcy\.get\(([^)]*)\)"
            r"(?:\.\w+\([^)]*\))*"
            r"\.(click|type|select|check|uncheck)\(([^)]*)\)",
            line,
        )
        if chain:
            selector = _first_string_literal(chain.group(1)) or chain.group(1).strip()
            action = chain.group(2)
            value = _first_string_literal(chain.group(3)) if action in ("type", "select") else None
            steps.extend(_action_from_js(action, selector, value))
            continue

        contains = re.search(
            r"\bcy\.contains\(([^)]*)\)"
            r"(?:\.\w+\([^)]*\))*"
            r"\.(click|type)\(([^)]*)\)",
            line,
        )
        if contains:
            # cy.contains('button', 'text') — first arg may be a tag, second is the text
            args_str = contains.group(1)
            parts = [p.strip() for p in args_str.split(",")]
            # Use the last string literal as the visible text selector
            selector = _first_string_literal(parts[-1]) or args_str.strip()
            action = contains.group(2)
            value = _first_string_literal(contains.group(3)) if action == "type" else None
            steps.extend(_action_from_js(action, selector, value))
            continue

    return steps


def _parse_webdriverio_js(body: str) -> list[ImportedStep]:
    """Parse WebdriverIO-style JS: browser.url(), $$('sel'), var[N].setValue/click."""
    steps: list[ImportedStep] = []
    locator_vars: dict[str, str] = {}

    for line in _normalize_chained_lines(body.splitlines()):
        line = line.strip()
        if not line:
            continue

        # browser.url('https://...') → go_to
        url_match = re.search(r"\bbrowser\.url\((.*)\)", line)
        if url_match:
            url = _first_string_literal(url_match.group(1))
            if url:
                steps.append(ImportedStep(ActionType.GO_TO, "go_to", url=_normalize_url(url, None)))
            continue

        # const varName = await $$('selector')  OR  const varName = await $('selector')
        assign = re.search(r"(?:const|let|var)\s+(\w+)\s*=\s*await\s+\$\$?\(([^)]*)\)", line)
        if assign:
            var = assign.group(1)
            selector = _first_string_literal(assign.group(2))
            if selector:
                locator_vars[var] = selector
            continue

        # varName[N].setValue('value') → send_keys
        set_val = re.search(r"(\w+)\[\d+\]\.setValue\((.*)\)", line)
        if set_val:
            var = set_val.group(1)
            value = _first_string_literal(set_val.group(2))
            selector = locator_vars.get(var)
            steps.append(ImportedStep(ActionType.SEND_KEYS, "send_keys", selector=selector, value=value))
            continue

        # varName[N].click() → click
        click_match = re.search(r"(\w+)\[\d+\]\.click\(\)", line)
        if click_match:
            var = click_match.group(1)
            selector = locator_vars.get(var)
            steps.append(ImportedStep(ActionType.CLICK, "click", selector=selector))
            continue

    return steps


_PW_LOCATOR_METHODS = (
    "locator", "get_by_text", "get_by_role", "get_by_label",
    "get_by_placeholder", "get_by_test_id", "get_by_alt_text", "get_by_title",
)

_PW_LOCATOR_RE = re.compile(
    r"(\w+)\s*=\s*page\.("
    + "|".join(_PW_LOCATOR_METHODS)
    + r")\(([^)]*)\)",
)

_PW_CHAIN_RE = re.compile(
    r"\bpage\.(?:"
    + "|".join(_PW_LOCATOR_METHODS)
    + r")\(([^)]*)\)(?:\.\w+\([^)]*\))*\.(click|fill|type|press|hover|select_option)\((.*)\)",
)

_PW_VAR_ACTION_RE = re.compile(
    r"(\w+)\.(click|fill|type|press|hover|select_option)\((.*)\)",
)


def _selector_from_locator_method(
    method: str,
    args_str: str,
    constants: "dict[str, str] | None" = None,
) -> str | None:
    """Build a CSS/text selector string from a page.get_by_*() or page.locator() call."""
    first_arg = _resolve_str_arg(args_str, constants or {})
    if not first_arg:
        return None
    if method == "locator":
        return first_arg
    if method == "get_by_text":
        return f"text={first_arg}"
    if method == "get_by_role":
        return f"role={first_arg}"
    if method == "get_by_placeholder":
        return f'[placeholder="{first_arg}"]'
    if method == "get_by_label":
        return f"label={first_arg}"
    if method == "get_by_test_id":
        return f'[data-testid="{first_arg}"]'
    if method == "get_by_alt_text":
        return f'[alt="{first_arg}"]'
    if method == "get_by_title":
        return f'[title="{first_arg}"]'
    return first_arg


def _parse_playwright_py(
    body: str,
    base_url: str | None,
    constants: "dict[str, str] | None" = None,
) -> list[ImportedStep]:
    steps: list[ImportedStep] = []
    locator_vars: dict[str, str] = {}
    _c = constants or {}

    for line in _normalize_chained_lines(_normalize_python_lines(body.splitlines())):
        line = line.strip()
        if not line:
            continue

        # page.goto(...)
        if "page.goto(" in line:
            args = line.split("page.goto(", 1)[1]
            url = _resolve_str_arg(args, _c)
            url = _resolve_base_url_literal(url, base_url)
            if url:
                steps.append(ImportedStep(ActionType.GO_TO, "go_to", url=_normalize_url(url, base_url)))
            continue

        # Locator variable assignment: var = page.locator(...) / page.get_by_*(...)
        loc_assign = _PW_LOCATOR_RE.search(line)
        if loc_assign:
            var_name = loc_assign.group(1)
            method = loc_assign.group(2)
            args_str = loc_assign.group(3)
            selector = _selector_from_locator_method(method, args_str, _c)
            if selector:
                locator_vars[var_name] = selector
            continue

        # Inline locator chain: page.locator(...).click() / page.get_by_text(...).fill(...)
        chain = _PW_CHAIN_RE.search(line)
        if chain:
            args_str = chain.group(1)
            action = chain.group(2)
            action_args = chain.group(3)
            # Determine the locator method from the original line
            method_match = re.search(r"\bpage\.(" + "|".join(_PW_LOCATOR_METHODS) + r")\(", line)
            method = method_match.group(1) if method_match else "locator"
            selector = _selector_from_locator_method(method, args_str, _c)
            value = _resolve_str_arg(action_args, _c) if action_args.strip() else None
            steps.extend(_action_from_py(action, selector, value))
            continue

        # Variable action: var.click() / var.fill("value")
        var_action = _PW_VAR_ACTION_RE.search(line)
        if var_action:
            var_name = var_action.group(1)
            action = var_action.group(2)
            action_args = var_action.group(3)
            selector = locator_vars.get(var_name)
            if selector is not None:
                value = _resolve_str_arg(action_args, _c) if action_args.strip() else None
                steps.extend(_action_from_py(action, selector, value))
                continue

        # Direct page actions: page.click(...), page.fill(...)
        call = re.search(r"\bpage\.(click|fill|type|press|hover|select_option)\((.*)\)", line)
        if call:
            action = call.group(1)
            args = call.group(2)
            parts = [p.strip() for p in args.split(",")]
            selector = _resolve_str_arg(parts[0], _c) if parts else None
            value = _resolve_str_arg(parts[1], _c) if len(parts) > 1 else None
            steps.extend(_action_from_py(action, selector, value))
            continue

        upload = re.search(r"\bpage\.set_input_files\((.*)\)", line)
        if upload:
            parts = [p.strip() for p in upload.group(1).split(",")]
            selector = _resolve_str_arg(parts[0], _c) if parts else None
            value = _resolve_str_arg(parts[1], _c) if len(parts) > 1 else None
            steps.append(ImportedStep(ActionType.UPLOAD, "Upload file", selector=selector, value=value))
            continue

        if "page.screenshot(" in line:
            steps.append(ImportedStep(ActionType.SCREENSHOT, "Screenshot"))

    return steps


def _normalize_python_lines(lines: list[str]) -> list[str]:
    normalized: list[str] = []
    buffer = ""
    for line in lines:
        stripped = line.rstrip()
        if stripped.endswith("\\"):
            buffer += stripped[:-1] + " "
            continue
        if buffer:
            normalized.append(buffer + stripped.lstrip())
            buffer = ""
        else:
            normalized.append(stripped)
    if buffer:
        normalized.append(buffer)
    return normalized


def _extract_url_from_glob(pattern: str) -> str | None:
    """Extract a readable path segment from a Playwright glob URL pattern like '**/path**'."""
    if not pattern:
        return None
    # Strip surrounding ** and /
    cleaned = pattern.strip("*").strip("/")
    return f"/{cleaned}" if cleaned else None


def _parse_playwright_py_assertions(
    body: str,
    base_url: str | None,
    constants: "dict[str, str] | None" = None,
) -> list[ImportedAssertion]:
    assertions: list[ImportedAssertion] = []
    lines = _normalize_python_lines(body.splitlines())
    locator_vars: dict[str, str] = {}
    _c = constants or {}

    for line in lines:
        line = line.strip()
        if not line:
            continue

        # Track locator variable assignments for later assertion resolution
        loc_assign = _PW_LOCATOR_RE.search(line)
        if loc_assign:
            var_name = loc_assign.group(1)
            method = loc_assign.group(2)
            args_str = loc_assign.group(3)
            selector = _selector_from_locator_method(method, args_str, _c)
            if selector:
                locator_vars[var_name] = selector
            continue

        # page.wait_for_url("**/path**", ...) → URL contains assertion
        wfu = re.search(r"\bpage\.wait_for_url\(([^,)]+)", line)
        if wfu:
            raw = _first_string_literal(wfu.group(1))
            if raw:
                url_path = _extract_url_from_glob(raw)
                if url_path:
                    assertions.append(
                        ImportedAssertion(
                            nature="page",
                            atype="to contain url",
                            value=url_path,
                            description="Verify page URL contains path",
                        )
                    )
            continue

        # Variable-based visibility: assert var.is_visible()
        var_vis = re.search(r"assert\s+(not\s+)?(\w+)\.is_visible\(\)", line)
        if var_vis:
            negated = var_vis.group(1)
            var_name = var_vis.group(2)
            selector = locator_vars.get(var_name)
            if selector:
                assertions.append(
                    ImportedAssertion(
                        nature="existence",
                        atype="not to be visible" if negated else "to be visible",
                        value=_human_value_from_selector(selector),
                        selector=selector,
                        description="Verify element visibility",
                    )
                )
                continue

        if not line.startswith("assert ") and "expect(" not in line:
            continue

        match = re.search(
            r"assert\s+(not\s+)?page\.locator\(([^)]*)\)\.is_visible",
            line,
        )
        if match:
            selector = _first_string_literal(match.group(2))
            if selector:
                assertions.append(
                    ImportedAssertion(
                        nature="existence",
                        atype="not to be visible" if match.group(1) else "to be visible",
                        value=_human_value_from_selector(selector),
                        selector=selector,
                        description="Verify element visibility",
                    )
                )
            continue

        # Inline get_by_* visibility: assert page.get_by_text("...").is_visible()
        inline_vis = re.search(
            r"assert\s+(not\s+)?page\.(" + "|".join(_PW_LOCATOR_METHODS) + r")\(([^)]*)\)\.is_visible",
            line,
        )
        if inline_vis:
            negated = inline_vis.group(1)
            method = inline_vis.group(2)
            args_str = inline_vis.group(3)
            selector = _selector_from_locator_method(method, args_str)
            if selector:
                assertions.append(
                    ImportedAssertion(
                        nature="existence",
                        atype="not to be visible" if negated else "to be visible",
                        value=_human_value_from_selector(selector),
                        selector=selector,
                        description="Verify element visibility",
                    )
                )
            continue

        match = re.search(r"assert\s+page\.input_value\(([^)]*)\)\s*==\s*(.*)", line)
        if match:
            selector = _first_string_literal(match.group(1))
            expected = _first_string_literal(match.group(2))
            if selector and expected is not None:
                assertions.append(
                    ImportedAssertion(
                        nature="input",
                        atype="to have value",
                        value=expected,
                        selector=selector,
                        description="Verify input value",
                    )
                )
            continue

        match = re.search(r"assert\s+page\.url\s*==\s*(.*)", line)
        if match:
            expected = _first_string_literal(match.group(1))
            expected = _resolve_base_url_literal(expected, base_url)
            if expected is not None:
                assertions.append(
                    ImportedAssertion(
                        nature="page",
                        atype="to have url",
                        value=expected,
                        description="Verify URL",
                    )
                )
            continue

        match = re.search(r"assert\s+(.*)\s+in\s+page\.url", line)
        if match:
            expected = _first_string_literal(match.group(1))
            expected = _resolve_base_url_literal(expected, base_url)
            if expected is not None:
                assertions.append(
                    ImportedAssertion(
                        nature="page",
                        atype="to contain url",
                        value=expected,
                        description="Verify URL contains",
                    )
                )
            continue

        # assert "/path" not in page.url
        match = re.search(r"assert\s+(.*)\s+not\s+in\s+page\.url", line)
        if match:
            expected = _first_string_literal(match.group(1))
            if expected is not None:
                assertions.append(
                    ImportedAssertion(
                        nature="page",
                        atype="to not contain url",
                        value=expected,
                        description="Verify URL does not contain",
                    )
                )
            continue

        match = re.search(r"assert\s+page\.title\(\)\s*==\s*(.*)", line)
        if match:
            expected = _first_string_literal(match.group(1))
            if expected is not None:
                assertions.append(
                    ImportedAssertion(
                        nature="page",
                        atype="to have title",
                        value=expected,
                        description="Verify page title",
                    )
                )
            continue

        # expect(...) style assertions (Playwright Python)
        match = re.search(
            r"expect\s*\(\s*page\.locator\(([^)]*)\)\s*\)\.(not_)?to_be_visible",
            line,
        )
        if match:
            selector = _first_string_literal(match.group(1))
            if selector:
                assertions.append(
                    ImportedAssertion(
                        nature="existence",
                        atype="not to be visible" if match.group(2) else "to be visible",
                        value=selector,
                        selector=selector,
                        description="Verify element visibility",
                    )
                )
            continue

        match = re.search(
            r"expect\s*\(\s*page\.locator\(([^)]*)\)\s*\)\.to_have_value\((.*)\)",
            line,
        )
        if match:
            selector = _first_string_literal(match.group(1))
            expected = _first_string_literal(match.group(2))
            if selector and expected is not None:
                assertions.append(
                    ImportedAssertion(
                        nature="input",
                        atype="to have value",
                        value=expected,
                        selector=selector,
                        description="Verify input value",
                    )
                )
            continue

        match = re.search(r"expect\s*\(\s*page\s*\)\.to_have_url\((.*)\)", line)
        if match:
            expected = _first_string_literal(match.group(1))
            expected = _resolve_base_url_literal(expected, base_url)
            if expected is not None:
                assertions.append(
                    ImportedAssertion(
                        nature="page",
                        atype="to have url",
                        value=expected,
                        description="Verify URL",
                    )
                )
            continue

        match = re.search(r"expect\s*\(\s*page\s*\)\.to_have_title\((.*)\)", line)
        if match:
            expected = _first_string_literal(match.group(1))
            if expected is not None:
                assertions.append(
                    ImportedAssertion(
                        nature="page",
                        atype="to have title",
                        value=expected,
                        description="Verify page title",
                    )
                )
            continue

    return assertions


def _parse_selenium_py(body: str) -> list[ImportedStep]:
    steps: list[ImportedStep] = []
    selector_vars: dict[str, str] = {}

    for line in body.splitlines():
        line = line.strip()
        if not line:
            continue

        find_match = re.search(r"(\w+)\s*=\s*driver\.find_element\((.*)\)", line)
        if find_match:
            var = find_match.group(1)
            selector = _first_string_literal(find_match.group(2)) or ""
            if selector:
                selector_vars[var] = selector
            continue

        if "driver.get(" in line:
            args = line.split("driver.get(", 1)[1]
            url = _first_string_literal(args)
            if url:
                steps.append(ImportedStep(ActionType.GO_TO, "go_to", url=_normalize_url(url, None)))
            continue

        if ".click()" in line:
            target = line.split(".click()", 1)[0]
            selector = selector_vars.get(target)
            steps.append(ImportedStep(ActionType.CLICK, "Click", selector=selector))
            continue

        send_keys = re.search(r"(\w+)\.send_keys\((.*)\)", line)
        if send_keys:
            target = send_keys.group(1)
            value = _first_string_literal(send_keys.group(2))
            selector = selector_vars.get(target)
            steps.append(ImportedStep(ActionType.SEND_KEYS, "send_keys", selector=selector, value=value))
            continue

        if "ActionChains" in line and "move_to_element" in line:
            steps.append(ImportedStep(ActionType.HOVER, "hover"))

    return steps


def _java_selector(by_type: str, value: str) -> str:
    by = by_type.lower()
    if by == "id":
        return f"#{value}"
    if by == "name":
        return f'[name="{value}"]'
    if by == "classname":
        return f".{value}"
    if by == "tagname":
        return value
    # cssSelector / xpath / linkText / partialLinkText fall back to raw value
    return value


def _parse_selenium_java(body: str) -> list[ImportedStep]:
    steps: list[ImportedStep] = []
    selector_vars: dict[str, str] = {}

    for raw in body.splitlines():
        line = raw.strip()
        if not line:
            continue

        # driver.get("https://...")
        if "driver.get(" in line:
            args = line.split("driver.get(", 1)[1]
            url = _first_string_literal(args)
            if url:
                steps.append(ImportedStep(ActionType.GO_TO, "go_to", url=_normalize_url(url, None)))
            continue

        # WebElement el = driver.findElement(By.id("username"));
        # List<WebElement> els = driver.findElements(By.cssSelector("..."));
        find_match = re.search(
            r"(?:(?:WebElement|List<WebElement>)\s+)?(\w+)\s*=\s*driver\.findElements?\(\s*By\.(\w+)\((.*?)\)\s*\)\s*;",
            line,
        )
        if find_match:
            var = find_match.group(1)
            by_type = find_match.group(2)
            selector_value = _first_string_literal(find_match.group(3)) or ""
            if selector_value:
                selector_vars[var] = _java_selector(by_type, selector_value)
            continue

        # driver.findElement(By.id("username")).sendKeys("value");
        inline_send = re.search(
            r"driver\.findElement\(\s*By\.(\w+)\((.*?)\)\s*\)\.sendKeys\((.*?)\)\s*;",
            line,
        )
        if inline_send:
            by_type = inline_send.group(1)
            selector_value = _first_string_literal(inline_send.group(2)) or ""
            value = _first_string_literal(inline_send.group(3))
            selector = _java_selector(by_type, selector_value) if selector_value else None
            steps.append(ImportedStep(ActionType.SEND_KEYS, "send_keys", selector=selector, value=value))
            continue

        # driver.findElement(By.id("username")).click();
        inline_click = re.search(
            r"driver\.findElement\(\s*By\.(\w+)\((.*?)\)\s*\)\.click\(\s*\)\s*;",
            line,
        )
        if inline_click:
            by_type = inline_click.group(1)
            selector_value = _first_string_literal(inline_click.group(2)) or ""
            selector = _java_selector(by_type, selector_value) if selector_value else None
            steps.append(ImportedStep(ActionType.CLICK, "Click", selector=selector))
            continue

        # var.get(N).sendKeys("value");  — List<WebElement> pattern
        get_send = re.search(r"(\w+)\.get\(\d+\)\.sendKeys\((.*?)\)\s*;", line)
        if get_send:
            var = get_send.group(1)
            value = _first_string_literal(get_send.group(2))
            selector = selector_vars.get(var)
            steps.append(ImportedStep(ActionType.SEND_KEYS, "send_keys", selector=selector, value=value))
            continue

        # var.get(N).click();  — List<WebElement> pattern
        get_click = re.search(r"(\w+)\.get\(\d+\)\.click\(\s*\)\s*;", line)
        if get_click:
            var = get_click.group(1)
            selector = selector_vars.get(var)
            steps.append(ImportedStep(ActionType.CLICK, "Click", selector=selector))
            continue

        # el.sendKeys("value");
        send_keys = re.search(r"(\w+)\.sendKeys\((.*?)\)\s*;", line)
        if send_keys:
            target = send_keys.group(1)
            value = _first_string_literal(send_keys.group(2))
            selector = selector_vars.get(target)
            steps.append(ImportedStep(ActionType.SEND_KEYS, "send_keys", selector=selector, value=value))
            continue

        # el.click();
        if ".click()" in line:
            target = line.split(".click()", 1)[0].strip()
            selector = selector_vars.get(target)
            if selector:
                steps.append(ImportedStep(ActionType.CLICK, "Click", selector=selector))
            continue

    return steps


def _action_from_js(action: str, selector: str | None, value: str | None) -> list[ImportedStep]:
    if action in ("click", "check", "uncheck"):
        return [ImportedStep(ActionType.CLICK, "click", selector=selector)]
    if action in ("fill", "type"):
        return [ImportedStep(ActionType.SEND_KEYS, "send_keys", selector=selector, value=value)]
    if action in ("select", "selectOption"):
        return [ImportedStep(ActionType.SELECT, "select", selector=selector, value=value)]
    if action == "hover":
        return [ImportedStep(ActionType.HOVER, "Hover", selector=selector)]
    return []


def _action_from_py(action: str, selector: str | None, value: str | None) -> list[ImportedStep]:
    if action == "click":
        return [ImportedStep(ActionType.CLICK, "click", selector=selector)]
    if action in ("fill", "type"):
        return [ImportedStep(ActionType.SEND_KEYS, "send_keys", selector=selector, value=value)]
    if action == "select_option":
        return [ImportedStep(ActionType.SELECT, "select", selector=selector, value=value)]
    if action == "press":
        return [ImportedStep(ActionType.PRESS, "press", selector=selector, value=value)]
    if action == "hover":
        return [ImportedStep(ActionType.HOVER, "Hover", selector=selector)]
    return []


def parse_test_file(path: Path, framework: str = "auto") -> list[ImportedTestCase]:
    content = path.read_text(encoding="utf-8", errors="ignore")
    cases: list[ImportedTestCase] = []

    if path.suffix.lower() in {".js", ".jsx", ".ts", ".tsx"}:
        tests = _extract_js_tests(content)
        if not tests:
            tests = _extract_script_tests(content, path.stem)
        for name, body in tests:
            if framework in ("auto", "playwright"):
                steps = _parse_playwright_js(body)
                if steps:
                    cases.append(ImportedTestCase(name, steps, [], "playwright", str(path)))
                    continue
            if framework in ("auto", "cypress"):
                steps = _parse_cypress_js(body)
                if steps:
                    cases.append(ImportedTestCase(name, steps, [], "cypress", str(path)))
                    continue
            if framework in ("auto", "webdriverio"):
                steps = _parse_webdriverio_js(body)
                if steps:
                    cases.append(ImportedTestCase(name, steps, [], "webdriverio", str(path)))
        return cases

    if path.suffix.lower() == ".py":
        base_url = _extract_base_url(content)
        constants = _extract_python_string_constants(content)
        tests = _extract_python_tests(content)
        if not tests:
            tests = _extract_script_tests(content, path.stem)
        for name, body in tests:
            if framework in ("auto", "playwright"):
                steps = _parse_playwright_py(body, base_url, constants)
                assertions = _parse_playwright_py_assertions(body, base_url, constants)
                if steps:
                    cases.append(ImportedTestCase(name, steps, assertions, "playwright", str(path)))
                    continue
            if framework in ("auto", "selenium"):
                steps = _parse_selenium_py(body)
                if steps:
                    cases.append(ImportedTestCase(name, steps, [], "selenium", str(path)))
        return cases

    # Non-Python/JS: skip for now
    if path.suffix.lower() == ".java":
        # Treat Java Selenium scripts as a single test case
        name = path.stem.replace("_", " ").replace("-", " ").strip() or "Test"
        if framework in ("auto", "selenium"):
            steps = _parse_selenium_java(content)
            if steps:
                return [ImportedTestCase(name, steps, [], "selenium", str(path))]
    return cases


def diagnose_no_tests(files: Iterable[Path]) -> str:
    py_files = 0
    js_files = 0
    py_defs = 0
    js_defs = 0

    for file in files:
        suffix = file.suffix.lower()
        if suffix == ".py":
            py_files += 1
        elif suffix in {".js", ".jsx", ".ts", ".tsx"}:
            js_files += 1
        else:
            continue

        content = file.read_text(encoding="utf-8", errors="ignore")
        if suffix == ".py":
            py_defs += _count_python_tests(content)
        else:
            js_defs += _count_js_tests(content)

    if py_files or js_files:
        if py_defs == 0 and js_defs == 0:
            return (
                "No test definitions found. For Python, functions must be named like "
                "`def test_*()`; for JS/TS, use `test(...)` or `it(...)` blocks."
            )
        if py_files and py_defs == 0:
            return "No Python `def test_*()` functions found in the provided files."
        if js_files and js_defs == 0:
            return "No JS/TS `test(...)` or `it(...)` blocks found in the provided files."
    return "No test definitions found in the provided files."


# Map AI-importer nature values → server-accepted values
_NATURE_MAP: dict[str, str] = {
    "url_check":   "page",
    "text_content": "existence",
    "input_value":  "input",
}

# Map AI-importer atype values → server-accepted values, keyed by (nature_after_map, atype)
_ATYPE_MAP: dict[tuple[str, str], str] = {
    ("page",      "equals"):   "to have url",
    ("page",      "contains"): "to contain url",
    ("input",     "equals"):   "to have value",
    ("input",     "contains"): "to have value",
    ("existence", "equals"):   "to be visible",
    ("existence", "contains"): "to be visible",
}


def _build_verification_payload(
    assertion: ImportedAssertion,
    page_url: str | None,
    page_name: str | None,
) -> dict:
    nature = _NATURE_MAP.get(assertion.nature, assertion.nature)
    atype  = _ATYPE_MAP.get((nature, assertion.atype), assertion.atype)
    payload: dict = {
        "type": "verification",
        "name": assertion.description or "Verify",
        "assertion_nature": nature,
        "assertion_type": atype,
    }
    if assertion.value is not None:
        payload["assertion_value"] = assertion.value
    if assertion.selector:
        payload["css_selector"] = assertion.selector
    if page_url:
        payload["page_url"] = page_url
    if page_name:
        payload["page_name"] = page_name
    return payload


def build_manual_steps_with_verifications(
    test_case: ImportedTestCase,
    base_url: str | None,
    page_name: str | None,
) -> list[dict]:
    steps: list[dict] = []
    current_url: str | None = None
    remaining = list(test_case.assertions)
    for step in test_case.steps:
        manual_action = ACTION_TO_MANUAL.get(step.action)
        if not manual_action:
            continue

        if step.action == ActionType.GO_TO:
            current_url = _normalize_url(step.url, base_url)
        elif step.url:
            current_url = _normalize_url(step.url, base_url)

        payload: dict = {
            "name": step.description,
            "actionType": manual_action,
        }
        if step.selector:
            payload["cssSelector"] = step.selector
        if current_url:
            payload["page_url"] = current_url
        if page_name:
            payload["page_name"] = page_name
        if step.action in (
            ActionType.SEND_KEYS,
            ActionType.PRESS,
            ActionType.SELECT,
            ActionType.UPLOAD,
        ):
            if step.value is not None:
                payload["mockInput"] = step.value
        if step.action == ActionType.GO_TO and current_url:
            payload["mockInput"] = current_url
        steps.append(payload)

        if step.selector and remaining:
            related = [a for a in remaining if a.selector and a.selector == step.selector]
            for assertion in related:
                steps.append(_build_verification_payload(assertion, current_url, page_name))
                remaining.remove(assertion)

    for assertion in remaining:
        steps.append(_build_verification_payload(assertion, current_url, page_name))
    return steps
