from __future__ import annotations

import importlib.util
import logging
import sys
import types
from pathlib import Path

import pytest


ROOT = Path(__file__).resolve().parents[4]


class _Response:
    status_code = 200
    text = ""

    def __init__(self, payload, status_code: int = 200):
        self._payload = payload
        self.status_code = status_code

    def json(self):
        return self._payload


def _load_docling_parser(monkeypatch):
    common_pkg = types.ModuleType("common")
    constants_mod = types.ModuleType("common.constants")
    constants_mod.MAXIMUM_PAGE_NUMBER = 1000

    deepdoc_pkg = types.ModuleType("deepdoc")
    parser_pkg = types.ModuleType("deepdoc.parser")
    parser_pkg.__path__ = []
    utils_mod = types.ModuleType("deepdoc.parser.utils")
    utils_mod.extract_pdf_outlines = lambda _source: []

    pil_pkg = types.ModuleType("PIL")
    image_mod = types.ModuleType("PIL.Image")
    image_mod.Image = object
    pil_pkg.Image = image_mod

    monkeypatch.setitem(sys.modules, "common", common_pkg)
    monkeypatch.setitem(sys.modules, "common.constants", constants_mod)
    monkeypatch.setitem(sys.modules, "deepdoc", deepdoc_pkg)
    monkeypatch.setitem(sys.modules, "deepdoc.parser", parser_pkg)
    monkeypatch.setitem(sys.modules, "deepdoc.parser.utils", utils_mod)
    monkeypatch.setitem(sys.modules, "pdfplumber", types.ModuleType("pdfplumber"))
    monkeypatch.setitem(sys.modules, "PIL", pil_pkg)
    monkeypatch.setitem(sys.modules, "PIL.Image", image_mod)

    spec = importlib.util.spec_from_file_location(
        "_docling_parser_under_test",
        ROOT / "deepdoc" / "parser" / "docling_parser.py",
    )
    module = importlib.util.module_from_spec(spec)
    monkeypatch.setitem(sys.modules, spec.name, module)
    spec.loader.exec_module(module)
    return module


@pytest.mark.p2
def test_remote_chunked_200_standard_payload_falls_back(monkeypatch):
    module = _load_docling_parser(monkeypatch)
    calls = []

    def fake_post(_url, json, timeout):
        calls.append((json, timeout))
        return _Response({"document": {"md_content": "# Parsed\n\nbody"}})

    monkeypatch.setattr(module.requests, "post", fake_post)

    parser = module.DoclingParser(docling_server_url="http://docling.local")
    sections, tables = parser._parse_pdf_remote("sample.pdf", binary=b"%PDF", parse_method="raw")

    assert sections == [("# Parsed\n\nbody", "")]
    assert tables == []
    assert calls[0][0]["options"]["do_chunking"] is True


@pytest.mark.p2
def test_chunk_shape_helper_recognises_chunk_payloads(monkeypatch):
    """A response that is chunk-shaped (list, or dict with non-empty results/chunks)
    is classified as chunked regardless of which payload was sent."""
    module = _load_docling_parser(monkeypatch)
    assert module.DoclingParser._looks_like_chunk_response([{"text": "chunk-1"}]) is True
    assert module.DoclingParser._looks_like_chunk_response({"results": [{"text": "chunk-1"}, {"text": "chunk-2"}]}) is True
    assert module.DoclingParser._looks_like_chunk_response({"chunks": [{"text": "chunk-1"}]}) is True


@pytest.mark.p2
def test_chunk_shape_helper_rejects_standard_payloads(monkeypatch):
    """A standard conversion response, empty containers, and non-payload types
    are correctly classified as not-chunked."""
    module = _load_docling_parser(monkeypatch)
    standard = {"document": {"md_content": "body"}, "status": "success"}
    assert module.DoclingParser._looks_like_chunk_response(standard) is False
    assert module.DoclingParser._looks_like_chunk_response({}) is False
    assert module.DoclingParser._looks_like_chunk_response({"results": []}) is False
    assert module.DoclingParser._looks_like_chunk_response({"chunks": []}) is False
    assert module.DoclingParser._looks_like_chunk_response([]) is False
    assert module.DoclingParser._looks_like_chunk_response("not-a-payload") is False
    assert module.DoclingParser._looks_like_chunk_response(None) is False
    assert module.DoclingParser._looks_like_chunk_response(42) is False


@pytest.mark.p2
def test_remote_chunked_request_with_results_list_is_treated_as_chunked(monkeypatch):
    """A server that returns a ``results`` list (Docling Serve's native chunk
    shape) is treated as chunked and each chunk becomes a section."""
    module = _load_docling_parser(monkeypatch)

    def fake_post(_url, json, timeout):
        return _Response({"results": [{"text": "alpha"}, {"text": "beta"}]})

    monkeypatch.setattr(module.requests, "post", fake_post)

    parser = module.DoclingParser(docling_server_url="http://docling.local")
    sections, tables = parser._parse_pdf_remote("sample.pdf", binary=b"%PDF", parse_method="raw")

    assert sections == [("alpha", ""), ("beta", "")]
    assert tables == []


@pytest.mark.p2
def test_remote_top_level_list_response_is_treated_as_chunked(monkeypatch):
    """A server that returns a top-level JSON array of chunks is treated
    as chunked (matches the existing implicit assumption in the code)."""
    module = _load_docling_parser(monkeypatch)

    def fake_post(_url, json, timeout):
        return _Response([{"text": "first"}, {"text": "second"}])

    monkeypatch.setattr(module.requests, "post", fake_post)

    parser = module.DoclingParser(docling_server_url="http://docling.local")
    sections, _ = parser._parse_pdf_remote("sample.pdf", binary=b"%PDF", parse_method="raw")

    assert sections == [("first", ""), ("second", "")]


@pytest.mark.p2
def test_remote_chunked_request_with_ignored_flag_does_not_log_success(monkeypatch, caplog):
    """When Docling Serve silently drops the ``do_chunking`` flag and returns
    a standard conversion response, RAGFlow must not log a chunking-success
    message and must log a warning instead."""
    module = _load_docling_parser(monkeypatch)

    def fake_post(_url, json, timeout):
        return _Response({"document": {"md_content": "real content"}, "status": "success"})

    monkeypatch.setattr(module.requests, "post", fake_post)

    parser = module.DoclingParser(docling_server_url="http://docling.local")
    with caplog.at_level(logging.DEBUG, logger="DoclingParser"):
        sections, _ = parser._parse_pdf_remote("sample.pdf", binary=b"%PDF", parse_method="raw")

    assert sections == [("real content", "")]
    flat = " ".join(record.getMessage() for record in caplog.records)
    assert "Successfully used native chunking" not in flat
    assert "Server ignored chunking request" in flat


@pytest.mark.p2
def test_crop_without_page_images_returns_none(monkeypatch):
    """Position tags are emitted even when page rendering failed and left
    ``page_images`` empty, so ``crop`` must bail out instead of raising
    IndexError while indexing the empty list."""
    module = _load_docling_parser(monkeypatch)
    parser = module.DoclingParser(docling_server_url="http://docling.local")
    parser.page_images = []

    tag = "@@1\t1.0\t2.0\t3.0\t4.0##"
    assert parser.crop(tag, need_position=True) == (None, None)
    assert parser.crop(tag) is None


@pytest.mark.p2
def test_crop_drops_positions_beyond_rendered_pages(monkeypatch):
    """A tag naming a page past the rendered range must be filtered out rather
    than indexed, mirroring the range check in cropout_docling_table."""
    module = _load_docling_parser(monkeypatch)
    parser = module.DoclingParser(docling_server_url="http://docling.local")
    # a single rendered page; the sentinel is never indexed because the
    # out-of-range position is dropped first.
    parser.page_images = [object()]

    tag = "@@5\t1.0\t2.0\t3.0\t4.0##"
    assert parser.crop(tag, need_position=True) == (None, None)
    assert parser.crop(tag) is None
