"""Unit tests for the GitHub Actions parser."""

from __future__ import annotations

import io
import json
import unittest
import urllib.request
import zipfile
from unittest.mock import patch

from github_actions_parser import (
    GitHubActionsError,
    extract_log_files,
    fetch_run_logs,
    parse_logs,
    parse_run_url,
)


def make_zip(files: dict[str, str]) -> bytes:
    """Build an in-memory ZIP suitable for mocked GitHub responses."""

    output = io.BytesIO()
    with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive:
        for name, text in files.items():
            archive.writestr(name, text)
    return output.getvalue()


class FakeResponse:
    """Small context-manager response used by the mocked API test."""

    def __init__(self, body: bytes) -> None:
        self.body = body

    def __enter__(self) -> "FakeResponse":
        return self

    def __exit__(self, *_args: object) -> None:
        return None

    def read(self) -> bytes:
        return self.body


class ParserTests(unittest.TestCase):
    """Cover URL parsing, mocked API I/O, and all required failure types."""

    def test_parse_run_url(self) -> None:
        self.assertEqual(
            parse_run_url("https://github.com/acme/demo/actions/runs/12345"),
            ("acme", "demo", "12345"),
        )

    def test_rejects_non_run_url(self) -> None:
        with self.assertRaises(GitHubActionsError):
            parse_run_url("https://github.com/acme/demo/issues/1")

    @patch("github_actions_parser.urllib.request.urlopen")
    def test_fetch_run_logs_uses_mocked_github_response(self, mocked_open: object) -> None:
        mocked_open.return_value = FakeResponse(make_zip({"1_build.txt": "##[error]error TS2322: type mismatch"}))  # type: ignore[attr-defined]
        payload = fetch_run_logs("https://github.com/acme/demo/actions/runs/7")
        self.assertEqual(extract_log_files(payload)["1_build.txt"], "##[error]error TS2322: type mismatch")
        called_url = mocked_open.call_args.args[0].full_url  # type: ignore[attr-defined]
        self.assertIn("/repos/acme/demo/actions/runs/7/logs", called_url)

    def test_parses_pytest_failure_and_step(self) -> None:
        logs = {
            "2_Test.txt": "##[group]Run pytest\nFAILED tests/test_api.py::test_ok - AssertionError: 401 != 200\n##[endgroup]",
        }
        report = parse_logs(logs)
        self.assertEqual(report["failure_type"], "test")
        self.assertEqual(report["suggested_fix_category"], "tests")
        self.assertEqual(report["failing_step"], "pytest")
        self.assertIn("AssertionError", report["error_message"])

    def test_parses_typescript_build_failure(self) -> None:
        report = parse_logs({"3_Build.txt": "Run npm run build\nerror TS2322: Type 'string' is not assignable"})
        self.assertEqual(report["failure_type"], "build")
        self.assertEqual(report["suggested_fix_category"], "build")

    def test_parses_lint_failure(self) -> None:
        report = parse_logs({"4_Lint.txt": "Run npm run lint\n##[error]no-unused-vars: x is defined but never used"})
        self.assertEqual(report["failure_type"], "lint")
        self.assertEqual(report["suggested_fix_category"], "lint")

    def test_no_marker_is_explicit(self) -> None:
        report = parse_logs({"1_Test.txt": "all checks completed successfully"})
        self.assertEqual(report["parse_status"], "no_failure_marker_found")
        self.assertIsNone(report["error_message"])


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