"""Tests for NATIVE tool handlers — db_query, http_request, file_parse, web_crawler."""

import json
import os
import tempfile
from unittest.mock import AsyncMock

import pytest

from qremotex.tools.db_query import handle_db_query
from qremotex.tools.file_parse import handle_file_parse
from qremotex.tools.http_request import handle_http_request
from qremotex.tools.registry import ToolRegistry, build_default_registry
from qremotex.tools.web_crawler import _LinkExtractor, _TextExtractor, handle_web_crawler

# ──────────────────────────── file_parse ─────────────────────────────


class TestFileParseHandler:
    """Test the file_parse NATIVE handler."""

    @pytest.mark.asyncio
    async def test_parse_json_file(self, tmp_path: object) -> None:
        f = tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False, dir=str(tmp_path))
        json.dump({"key": "value", "num": 42}, f)
        f.close()
        try:
            result = await handle_file_parse({"file_path": f.name}, {})
            assert result["format"] == "json"
            assert result["content"]["key"] == "value"
        finally:
            os.unlink(f.name)

    @pytest.mark.asyncio
    async def test_parse_csv_file(self, tmp_path: object) -> None:
        f = tempfile.NamedTemporaryFile(suffix=".csv", mode="w", delete=False, dir=str(tmp_path))
        f.write("name,age\nAlice,30\nBob,25\n")
        f.close()
        try:
            result = await handle_file_parse({"file_path": f.name, "format": "csv"}, {})
            assert result["format"] == "csv"
            assert result["row_count"] == 2
            assert result["content"][0]["name"] == "Alice"
        finally:
            os.unlink(f.name)

    @pytest.mark.asyncio
    async def test_parse_text_file(self, tmp_path: object) -> None:
        f = tempfile.NamedTemporaryFile(suffix=".txt", mode="w", delete=False, dir=str(tmp_path))
        f.write("hello world\n")
        f.close()
        try:
            result = await handle_file_parse({"file_path": f.name, "format": "text"}, {})
            assert result["format"] == "text"
            assert "hello world" in result["content"]
        finally:
            os.unlink(f.name)

    @pytest.mark.asyncio
    async def test_parse_lines_format(self, tmp_path: object) -> None:
        f = tempfile.NamedTemporaryFile(suffix=".log", mode="w", delete=False, dir=str(tmp_path))
        f.write("line1\nline2\nline3\n")
        f.close()
        try:
            result = await handle_file_parse({"file_path": f.name, "format": "lines"}, {})
            assert result["format"] == "lines"
            assert result["line_count"] == 3
        finally:
            os.unlink(f.name)

    @pytest.mark.asyncio
    async def test_missing_path(self) -> None:
        result = await handle_file_parse({}, {})
        assert "error" in result

    @pytest.mark.asyncio
    async def test_nonexistent_file(self) -> None:
        result = await handle_file_parse({"file_path": "/nonexistent/file.json"}, {})
        assert "error" in result

    @pytest.mark.asyncio
    async def test_max_lines_limit(self, tmp_path: object) -> None:
        f = tempfile.NamedTemporaryFile(suffix=".log", mode="w", delete=False, dir=str(tmp_path))
        for i in range(100):
            f.write(f"line{i}\n")
        f.close()
        try:
            result = await handle_file_parse(
                {"file_path": f.name, "format": "lines", "max_lines": 5}, {}
            )
            assert result["line_count"] == 5
            assert result["truncated"] is True
        finally:
            os.unlink(f.name)

    @pytest.mark.asyncio
    async def test_unsupported_format(self, tmp_path: object) -> None:
        f = tempfile.NamedTemporaryFile(suffix=".xyz", mode="w", delete=False, dir=str(tmp_path))
        f.write("data")
        f.close()
        try:
            # "binary" is not a supported format, but falls through to text
            # Test with file_path param name
            result = await handle_file_parse({"file_path": f.name, "format": "binary"}, {})
            # binary would be caught as unsupported or fall through to text
            # file_parse treats unknown formats as text
            assert result.get("format") == "text" or "error" in result
        finally:
            os.unlink(f.name)


# ──────────────────────────── http_request ───────────────────────────


class TestHttpRequestHandler:
    """Test the http_request NATIVE handler."""

    @pytest.mark.asyncio
    async def test_missing_url(self) -> None:
        result = await handle_http_request({}, {})
        assert "error" in result

    @pytest.mark.asyncio
    async def test_invalid_scheme(self) -> None:
        result = await handle_http_request({"url": "ftp://example.com"}, {})
        assert "error" in result

    @pytest.mark.asyncio
    async def test_ssrf_blocked_loopback(self) -> None:
        result = await handle_http_request({"url": "http://127.0.0.1/secret"}, {})
        assert "error" in result
        assert "SSRF" in result["error"] or "blocked" in result["error"]

    @pytest.mark.asyncio
    async def test_ssrf_blocked_private(self) -> None:
        result = await handle_http_request({"url": "http://10.0.0.5/internal"}, {})
        assert "error" in result

    @pytest.mark.asyncio
    async def test_ssrf_allowed_override(self) -> None:
        """When allow_private_ips is True, private IPs should be allowed through."""
        # This won't actually connect (no server), but shouldn't fail on SSRF check.
        result = await handle_http_request(
            {"url": "http://10.0.0.5/internal", "allow_private_ips": True}, {}
        )
        # Should fail with connection error, NOT SSRF
        assert "error" in result
        assert "SSRF" not in result.get("error", "")


# ──────────────────────────── db_query ───────────────────────────────


class TestDbQueryHandler:
    """Test the db_query NATIVE handler."""

    @pytest.mark.asyncio
    async def test_missing_query(self) -> None:
        result = await handle_db_query(
            {"connection_string": "sqlite:///test.db"}, {}
        )
        assert "error" in result

    @pytest.mark.asyncio
    async def test_missing_connection_string(self) -> None:
        result = await handle_db_query({"query": "SELECT 1"}, {})
        assert "error" in result

    @pytest.mark.asyncio
    async def test_unsupported_driver(self) -> None:
        result = await handle_db_query(
            {"connection_string": "oracle://host/db", "query": "SELECT 1"}, {}
        )
        assert "error" in result
        assert "Unsupported" in result["error"]


# ──────────────────────────── web_crawler ────────────────────────────


class TestWebCrawlerHandler:
    """Test the web_crawler NATIVE handler."""

    @pytest.mark.asyncio
    async def test_missing_url(self) -> None:
        result = await handle_web_crawler({}, {})
        assert "error" in result

    @pytest.mark.asyncio
    async def test_invalid_scheme(self) -> None:
        result = await handle_web_crawler({"url": "ftp://example.com"}, {})
        assert "error" in result and "Unsupported" in result["error"]

    @pytest.mark.asyncio
    async def test_invalid_extract_mode(self) -> None:
        result = await handle_web_crawler(
            {"url": "https://example.com", "extract": "binary"}, {}
        )
        assert "error" in result and "Unsupported extract" in result["error"]

    def test_text_extractor(self) -> None:
        """Test the HTML text extractor utility."""
        html = "<html><body><h1>Title</h1><script>var x=1;</script><p>Content here</p></body></html>"
        ex = _TextExtractor()
        ex.feed(html)
        text = ex.get_text()
        assert "Title" in text
        assert "Content here" in text
        assert "var x" not in text  # script tags stripped

    def test_link_extractor(self) -> None:
        """Test the HTML link extractor utility."""
        html = '<a href="/page1">Link 1</a><a href="https://other.com">External</a>'
        ex = _LinkExtractor("https://example.com/")
        ex.feed(html)
        assert "https://example.com/page1" in ex.links
        assert "https://other.com" in ex.links


# ──────────────────────────── registry ───────────────────────────────


class TestToolRegistry:
    """Test the tool handler registry."""

    def test_register_and_get(self) -> None:
        reg = ToolRegistry()
        handler = AsyncMock()
        reg.register("test.tool", handler)
        assert reg.has("test.tool")
        assert reg.get("test.tool") is handler

    def test_get_missing(self) -> None:
        reg = ToolRegistry()
        assert reg.get("nonexistent") is None

    def test_has_missing(self) -> None:
        reg = ToolRegistry()
        assert not reg.has("nonexistent")

    def test_registered_keys(self) -> None:
        reg = ToolRegistry()
        reg.register("a", AsyncMock())
        reg.register("b", AsyncMock())
        assert set(reg.registered_keys) == {"a", "b"}

    def test_build_default_registry_has_all_handlers(self) -> None:
        reg = build_default_registry()
        expected = [
            "system.db_query",
            "system.http_request",
            "system.file_parse",
            "system.web_crawler",
        ]
        for key in expected:
            assert reg.has(key), f"Missing handler: {key}"

    def test_duplicate_register_overwrites(self) -> None:
        reg = ToolRegistry()
        handler1 = AsyncMock()
        handler2 = AsyncMock()
        reg.register("test.tool", handler1)
        reg.register("test.tool", handler2)
        assert reg.get("test.tool") is handler2
