from __future__ import annotations

import contextlib
import io
import json
import sys
import unittest
from unittest.mock import patch

import kyotool_fetch.cli as cli


class FakeResponse:
    def __init__(
        self,
        status_code: int = 200,
        text: str = "hello",
        url: str = "https://example.com/",
        headers: dict[str, str] | None = None,
    ) -> None:
        self.status_code = status_code
        self.text = text
        self.url = url
        self.headers = headers or {"content-type": "text/plain"}


class FakeClient:
    def __init__(self, *args, **kwargs) -> None:
        self.response = FakeResponse()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False

    def get(self, url: str):
        return self.response


class TimeoutClient(FakeClient):
    def get(self, url: str):
        raise cli.httpx.TimeoutException("timeout")


class ErrorClient(FakeClient):
    def get(self, url: str):
        return FakeResponse(status_code=500)


class TestParser(unittest.TestCase):
    def test_build_parser_exposes_expected_arguments(self) -> None:
        parser = cli.build_parser()

        self.assertEqual(parser.prog, "kyotool-fetch")
        self.assertEqual(parser.parse_args(["https://example.com"]).url, "https://example.com")
        self.assertEqual(parser.parse_args(["https://example.com"]).format, "markdown")

    def test_build_parser_rejects_invalid_format(self) -> None:
        parser = cli.build_parser()

        with self.assertRaises(SystemExit):
            parser.parse_args(["--format", "xml", "https://example.com"])


class TestFetchUrl(unittest.TestCase):
    def test_fetch_url_success_returns_headers_when_requested(self) -> None:
        with patch.object(cli.httpx, "Client", FakeClient):
            result = cli.fetch_url("https://example.com", "markdown", 20.0, True)

        self.assertTrue(result.ok)
        self.assertEqual(result.url, "https://example.com")
        self.assertEqual(result.final_url, "https://example.com/")
        self.assertEqual(result.content, "hello")
        self.assertEqual(result.text_length, 5)
        self.assertEqual(result.metadata["response_headers"]["content-type"], "text/plain")

    def test_fetch_url_http_error_returns_failure(self) -> None:
        with patch.object(cli.httpx, "Client", ErrorClient):
            result = cli.fetch_url("https://example.com", "markdown", 20.0, False)

        self.assertFalse(result.ok)
        self.assertEqual(result.status_code, 500)
        self.assertEqual(result.error, "HTTP 500")

    def test_fetch_url_timeout_returns_failure(self) -> None:
        with patch.object(cli.httpx, "Client", TimeoutClient):
            result = cli.fetch_url("https://example.com", "markdown", 20.0, False)

        self.assertFalse(result.ok)
        self.assertEqual(result.error, "request timed out")


class TestMain(unittest.TestCase):
    def test_main_default_prints_content(self) -> None:
        fake_result = cli.FetchResult(
            ok=True,
            url="https://example.com",
            final_url="https://example.com/",
            status_code=200,
            content_type="text/plain",
            title="",
            byline=None,
            excerpt=None,
            format="markdown",
            content="hello",
            text_length=5,
            links=[],
            metadata={},
            error=None,
        )

        stdout = io.StringIO()
        stderr = io.StringIO()
        with (
            patch.object(cli, "fetch_url", return_value=fake_result),
            patch.object(sys, "argv", ["kyotool-fetch", "https://example.com"]),
            contextlib.redirect_stdout(stdout),
            contextlib.redirect_stderr(stderr),
        ):
            exit_code = cli.main()

        self.assertEqual(exit_code, 0)
        self.assertEqual(stdout.getvalue().strip(), "hello")
        self.assertEqual(stderr.getvalue(), "")

    def test_main_json_prints_json(self) -> None:
        fake_result = cli.FetchResult(
            ok=True,
            url="https://example.com",
            final_url="https://example.com/",
            status_code=200,
            content_type="text/plain",
            title="제목",
            byline=None,
            excerpt=None,
            format="markdown",
            content="본문",
            text_length=2,
            links=[{"text": "링크", "href": "https://example.com/a"}],
            metadata={},
            error=None,
        )

        stdout = io.StringIO()
        with (
            patch.object(cli, "fetch_url", return_value=fake_result),
            patch.object(sys, "argv", ["kyotool-fetch", "https://example.com", "--json"]),
            contextlib.redirect_stdout(stdout),
        ):
            exit_code = cli.main()

        payload = json.loads(stdout.getvalue())
        self.assertEqual(exit_code, 0)
        self.assertEqual(payload["title"], "제목")
        self.assertEqual(payload["content"], "본문")
        self.assertEqual(payload["links"][0]["text"], "링크")

    def test_main_links_only_takes_priority_over_json(self) -> None:
        fake_result = cli.FetchResult(
            ok=True,
            url="https://example.com",
            final_url="https://example.com/",
            status_code=200,
            content_type="text/plain",
            title="Title",
            byline=None,
            excerpt=None,
            format="markdown",
            content="hello",
            text_length=5,
            links=[{"text": "A", "href": "https://example.com/a"}],
            metadata={},
            error=None,
        )

        stdout = io.StringIO()
        with (
            patch.object(cli, "fetch_url", return_value=fake_result),
            patch.object(
                sys,
                "argv",
                ["kyotool-fetch", "https://example.com", "--links-only", "--json"],
            ),
            contextlib.redirect_stdout(stdout),
        ):
            exit_code = cli.main()

        payload = json.loads(stdout.getvalue())
        self.assertEqual(exit_code, 0)
        self.assertEqual(
            set(payload.keys()),
            {"ok", "url", "final_url", "status_code", "title", "links", "error"},
        )

    def test_main_failure_writes_stderr(self) -> None:
        fake_result = cli.FetchResult(
            ok=False,
            url="https://example.com",
            final_url="https://example.com/",
            status_code=500,
            content_type="text/plain",
            title="",
            byline=None,
            excerpt=None,
            format="markdown",
            content="",
            text_length=0,
            links=[],
            metadata={},
            error="HTTP 500",
        )

        stdout = io.StringIO()
        stderr = io.StringIO()
        with (
            patch.object(cli, "fetch_url", return_value=fake_result),
            patch.object(sys, "argv", ["kyotool-fetch", "https://example.com"]),
            contextlib.redirect_stdout(stdout),
            contextlib.redirect_stderr(stderr),
        ):
            exit_code = cli.main()

        self.assertEqual(exit_code, 1)
        self.assertEqual(stdout.getvalue(), "")
        self.assertEqual(stderr.getvalue().strip(), "ERROR: HTTP 500")


if __name__ == "__main__":
    unittest.main()
