"""Unit tests for src.extraction_v2.ks_parser adapter.

Uses openpyxl to create fixture xlsx files in tmp_path so no external
test data files are needed.
"""
from __future__ import annotations

import re
import textwrap
from pathlib import Path
from typing import List
from unittest.mock import MagicMock, patch

import openpyxl
import pytest


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_simple_xlsx(tmp_path: Path, sheet_data: dict[str, list[list]]) -> Path:
    """Create a minimal xlsx file with the given sheet data.

    ``sheet_data`` maps sheet_name -> list of rows, each row is a list of
    cell values (str or None).
    """
    wb = openpyxl.Workbook()
    # Remove default 'Sheet' sheet if we're providing our own
    first = True
    for sheet_name, rows in sheet_data.items():
        if first:
            ws = wb.active
            ws.title = sheet_name
            first = False
        else:
            ws = wb.create_sheet(title=sheet_name)
        for r_idx, row in enumerate(rows, start=1):
            for c_idx, val in enumerate(row, start=1):
                if val is not None:
                    ws.cell(row=r_idx, column=c_idx, value=val)
    out = tmp_path / "fixture.xlsx"
    wb.save(str(out))
    return out


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestKsparserToSheetsBasic:
    """Basic structural tests for ksparser_to_sheets."""

    def test_returns_list(self, tmp_path):
        """ksparser_to_sheets returns a list (possibly empty) without crashing."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        xlsx = _make_simple_xlsx(tmp_path, {"Sheet1": [["hello", "world"], ["foo", "bar"]]})
        result = ksparser_to_sheets(str(xlsx))
        assert isinstance(result, list)

    def test_sheet_name_key_present(self, tmp_path):
        """Each entry in the result has 'sheet_name' and 'markdown' keys."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        xlsx = _make_simple_xlsx(tmp_path, {"MySheet": [["col1", "col2"], ["a", "b"]]})
        result = ksparser_to_sheets(str(xlsx))
        for entry in result:
            assert "sheet_name" in entry, "entry missing 'sheet_name'"
            assert "markdown" in entry, "entry missing 'markdown'"

    def test_markdown_is_string(self, tmp_path):
        """markdown value is a non-empty string for sheets with content."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        xlsx = _make_simple_xlsx(tmp_path, {"Sheet1": [["value"]]})
        result = ksparser_to_sheets(str(xlsx))
        for entry in result:
            assert isinstance(entry["markdown"], str)
            assert entry["markdown"].strip(), "markdown should not be empty for non-empty sheet"

    def test_per_sheet_split_multi_sheet(self, tmp_path):
        """Multi-sheet workbook produces one entry per non-empty sheet."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        xlsx = _make_simple_xlsx(tmp_path, {
            "Alpha": [["content alpha"]],
            "Beta": [["content beta"]],
        })
        result = ksparser_to_sheets(str(xlsx))
        assert len(result) >= 1, "expected at least one sheet entry"
        sheet_names = [e["sheet_name"] for e in result]
        # Both sheets have content; both should appear
        for expected in ("Alpha", "Beta"):
            assert expected in sheet_names, f"sheet {expected!r} missing from result"

    def test_content_appears_in_markdown(self, tmp_path):
        """Cell text appears somewhere in the markdown output (accounting for md escaping)."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        # Use text without special markdown characters to avoid escape issues
        unique_text = "UNIQUE FIXTURE VALUE ABC"
        xlsx = _make_simple_xlsx(tmp_path, {"Sheet1": [[unique_text]]})
        result = ksparser_to_sheets(str(xlsx))
        all_md = "\n".join(e["markdown"] for e in result)
        assert unique_text in all_md, "cell content should appear in markdown"

    def test_empty_workbook_returns_empty_list(self, tmp_path):
        """A workbook with no cell content returns an empty list."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        xlsx = _make_simple_xlsx(tmp_path, {"Empty": [[None, None], [None, None]]})
        result = ksparser_to_sheets(str(xlsx))
        # Sheets with no text content should be filtered out
        assert isinstance(result, list)
        for entry in result:
            assert entry["markdown"].strip(), "filtered-in entry must have non-empty markdown"


class TestKsparserToSheetsMermaid:
    """Mermaid fence restoration tests."""

    def test_mermaid_fence_preserved(self, tmp_path):
        """A cell containing a ```mermaid block is restored as a fenced code block."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        mermaid_text = "```mermaid\nflowchart TD\n  A --> B\n```"
        xlsx = _make_simple_xlsx(tmp_path, {
            "Sheet1": [
                ["Header"],
                [mermaid_text],
                ["Footer"],
            ]
        })
        result = ksparser_to_sheets(str(xlsx))
        all_md = "\n".join(e["markdown"] for e in result)
        # The mermaid fence should survive conversion
        assert "```mermaid" in all_md, "mermaid opening fence missing"
        assert "flowchart TD" in all_md, "mermaid diagram content missing"
        assert "A --> B" in all_md, "mermaid arrow syntax should be present"

    def test_mermaid_newlines_restored(self, tmp_path):
        """Mermaid block is not collapsed to a single line."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        mermaid_text = "```mermaid\nflowchart TD\n  Start --> End\n```"
        xlsx = _make_simple_xlsx(tmp_path, {"Sheet1": [[mermaid_text]]})
        result = ksparser_to_sheets(str(xlsx))
        all_md = "\n".join(e["markdown"] for e in result)
        # Should have a newline between ```mermaid and flowchart TD
        assert re.search(r"```mermaid\s*\n\s*flowchart", all_md), (
            "mermaid block should have proper newlines, not be collapsed"
        )


class TestKsparserToSheetsFormatting:
    """Tests for bold/color span preservation."""

    def test_bold_cell_produces_bold_markdown(self, tmp_path):
        """A cell with bold font should produce **bold** markdown."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = "Sheet1"
        ws["A1"] = "BoldText"
        ws["A1"].font = openpyxl.styles.Font(bold=True)
        xlsx = tmp_path / "bold_fixture.xlsx"
        wb.save(str(xlsx))

        result = ksparser_to_sheets(str(xlsx))
        all_md = "\n".join(e["markdown"] for e in result)
        # Bold cells should produce **..** in markdown
        assert "BoldText" in all_md, "bold cell text should appear in output"
        # ks_xlsx_parser emits font-weight:bold in style; _FormattedConverter wraps it
        # Either **BoldText** markdown or <b>BoldText</b> inline HTML is acceptable
        has_bold_md = "**BoldText**" in all_md or "<b>" in all_md
        assert has_bold_md, f"expected bold formatting in output, got:\n{all_md[:500]}"

    def test_no_crash_on_normal_content(self, tmp_path):
        """Normal text cells do not raise and produce non-empty markdown."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        xlsx = _make_simple_xlsx(tmp_path, {
            "Sheet1": [
                ["項目", "値"],
                ["パッケージ", "com.example"],
                ["クラス", "SampleClass"],
            ]
        })
        result = ksparser_to_sheets(str(xlsx))
        assert result, "expected at least one sheet in result"
        for entry in result:
            assert entry["markdown"].strip()


class TestKsparserToSheetsErrorHandling:
    """Error handling tests."""

    def test_corrupted_path_raises_or_returns_gracefully(self, tmp_path):
        """A non-existent path should raise an exception (ks_xlsx_parser will raise)."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        with pytest.raises(Exception):
            ksparser_to_sheets(str(tmp_path / "nonexistent.xlsx"))


class TestKsparserToSheetsSheetOrder:
    """Sheet order follows openpyxl sheetnames."""

    def test_sheet_order_matches_workbook_order(self, tmp_path):
        """Sheets appear in workbook order in the result list."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets

        xlsx = _make_simple_xlsx(tmp_path, {
            "First": [["alpha"]],
            "Second": [["beta"]],
            "Third": [["gamma"]],
        })
        result = ksparser_to_sheets(str(xlsx))
        result_names = [e["sheet_name"] for e in result]
        # All three must appear; order must be First, Second, Third
        wb = openpyxl.load_workbook(str(xlsx), read_only=True)
        expected_order = [n for n in wb.sheetnames if n in result_names]
        wb.close()
        assert result_names == expected_order, (
            f"expected sheet order {expected_order}, got {result_names}"
        )


class TestKsparserImport:
    """Smoke tests for package-level imports."""

    def test_import_ksparser_to_sheets(self):
        """ksparser_to_sheets can be imported from the package."""
        from src.extraction_v2.ks_parser import ksparser_to_sheets
        assert callable(ksparser_to_sheets)

    def test_import_ksparser_to_md(self):
        """ksparser_to_md can be imported from the package."""
        from src.extraction_v2.ks_parser import ksparser_to_md
        assert callable(ksparser_to_md)

    def test_no_sys_path_hack_in_package(self):
        """The ks_parser package does not do sys.path manipulation at import."""
        import sys
        original_path = list(sys.path)
        # Re-import to ensure no side effects
        import importlib
        import src.extraction_v2.ks_parser.pipeline as pipe_mod
        importlib.reload(pipe_mod)
        # sys.path should not have grown with REPO_ROOT hacks
        # (allow normal Python path items but reject explicit REPO_ROOT injection)
        new_items = [p for p in sys.path if p not in original_path]
        for item in new_items:
            assert "REPO_ROOT" not in str(item), (
                f"sys.path was modified with REPO_ROOT-style hack: {item}"
            )
