"""TestCaseFileParser — parse Excel/CSV test case files into structured models.

Handles flexible column naming by fuzzy-matching common test case headers.
"""

from __future__ import annotations

import csv
import logging
import re
from pathlib import Path
from typing import Any

from openpyxl import load_workbook
from pydantic import BaseModel, Field

log = logging.getLogger("antiqa.jira")


# ── Data models ────────────────────────────────────────

class TestStep(BaseModel):
    step_number: int
    action: str
    expected_result: str = ""
    test_data: str | None = None


class TestCase(BaseModel):
    id: str
    title: str
    preconditions: str | None = None
    steps: list[TestStep] = Field(default_factory=list)
    priority: str | None = None


# ── Column mapping ─────────────────────────────────────

# Canonical column names -> list of common aliases
_COLUMN_ALIASES: dict[str, list[str]] = {
    "id": ["test case id", "tc id", "id", "test_case_id", "tc_id", "case id", "case_id"],
    "title": ["title", "test case name", "name", "test name", "summary", "test_case_name"],
    "precondition": ["precondition", "preconditions", "pre-condition", "pre condition", "setup"],
    "step_number": ["step #", "step", "step number", "step_number", "step no", "#", "s.no", "sno"],
    "action": ["action", "description", "test step", "test_step", "steps", "action/description",
               "action description", "step description"],
    "expected_result": ["expected result", "expected", "expected_result", "expected outcome",
                        "expected results", "result"],
    "test_data": ["test data", "test_data", "data", "input", "input data", "test input"],
    "priority": ["priority", "severity", "importance"],
}


def _normalize(header: str) -> str:
    """Lowercase, strip, collapse whitespace."""
    return re.sub(r"\s+", " ", header.strip().lower())


def _map_columns(headers: list[str]) -> dict[str, int]:
    """Map canonical column names to column indices based on fuzzy matching."""
    mapping: dict[str, int] = {}
    normalized = [_normalize(h) for h in headers]

    for canonical, aliases in _COLUMN_ALIASES.items():
        for alias in aliases:
            for i, nh in enumerate(normalized):
                if nh == alias and canonical not in mapping:
                    mapping[canonical] = i
                    break
            if canonical in mapping:
                break

    return mapping


# ── Parsers ────────────────────────────────────────────

class TestCaseFileParser:
    """Parse Excel (.xlsx) and CSV test case files."""

    def parse(self, path: str | Path) -> list[TestCase]:
        p = Path(path)
        if p.suffix.lower() in (".xlsx", ".xls"):
            return self._parse_excel(p)
        if p.suffix.lower() == ".csv":
            return self._parse_csv(p)
        raise ValueError(f"Unsupported file type: {p.suffix}")

    def parse_many(self, paths: list[Path]) -> list[TestCase]:
        """Parse multiple files and return a combined list of test cases."""
        cases: list[TestCase] = []
        for p in paths:
            try:
                cases.extend(self.parse(p))
            except Exception as exc:
                log.warning("Failed to parse %s: %s", p.name, exc)
        return cases

    # ── Excel ──────────────────────────────────────────

    def _parse_excel(self, path: Path) -> list[TestCase]:
        wb = load_workbook(path, read_only=True, data_only=True)
        cases: list[TestCase] = []

        for ws in wb.worksheets:
            rows = list(ws.iter_rows(values_only=True))
            if not rows:
                continue

            headers = [str(c or "") for c in rows[0]]
            col_map = _map_columns(headers)

            if "action" not in col_map:
                log.warning("Sheet '%s' has no action/step column, skipping", ws.title)
                continue

            cases.extend(self._rows_to_cases(rows[1:], col_map))

        wb.close()
        log.info("Parsed %d test case(s) from %s", len(cases), path.name)
        return cases

    # ── CSV ────────────────────────────────────────────

    def _parse_csv(self, path: Path) -> list[TestCase]:
        with open(path, newline="", encoding="utf-8-sig") as f:
            reader = csv.reader(f)
            rows = list(reader)

        if not rows:
            return []

        headers = rows[0]
        col_map = _map_columns(headers)

        if "action" not in col_map:
            raise ValueError(f"CSV has no recognisable action/step column. Headers: {headers}")

        cases = self._rows_to_cases([tuple(r) for r in rows[1:]], col_map)
        log.info("Parsed %d test case(s) from %s", len(cases), path.name)
        return cases

    # ── Shared row processing ──────────────────────────

    def _rows_to_cases(self, rows: list[tuple[Any, ...]], col_map: dict[str, int]) -> list[TestCase]:
        """Convert raw rows into TestCase objects, grouping steps by test case ID/title."""
        cases: list[TestCase] = []
        current: TestCase | None = None

        for row in rows:
            if not row or all(c is None or str(c).strip() == "" for c in row):
                continue

            tc_id = self._get(row, col_map, "id")
            title = self._get(row, col_map, "title")

            # New test case starts when id or title is present
            if tc_id or title:
                current = TestCase(
                    id=tc_id or f"TC-{len(cases)+1}",
                    title=title or tc_id or "Untitled",
                    preconditions=self._get(row, col_map, "precondition") or None,
                    priority=self._get(row, col_map, "priority") or None,
                )
                cases.append(current)

            if current is None:
                continue

            action = self._get(row, col_map, "action")
            if not action:
                continue

            step_num_raw = self._get(row, col_map, "step_number")
            try:
                step_num = int(step_num_raw) if step_num_raw else len(current.steps) + 1
            except (ValueError, TypeError):
                step_num = len(current.steps) + 1

            current.steps.append(TestStep(
                step_number=step_num,
                action=action,
                expected_result=self._get(row, col_map, "expected_result"),
                test_data=self._get(row, col_map, "test_data") or None,
            ))

        return cases

    @staticmethod
    def _get(row: tuple[Any, ...], col_map: dict[str, int], key: str) -> str:
        idx = col_map.get(key)
        if idx is None or idx >= len(row):
            return ""
        val = row[idx]
        return str(val).strip() if val is not None else ""
