import os from collections import Counter from datetime import date from unittest.mock import patch, MagicMock from io import BytesIO from PIL import Image as PILImage, ImageDraw, ImageFont from src.cover import ( generate_programmatic_cover, generate_ai_cover, generate_cover, _get_dominant_category, _truncate_to_width, _get_font, CATEGORY_PROMPTS, ) def test_programmatic_cover_is_portrait(tmp_path): path = generate_programmatic_cover( output_dir=str(tmp_path), week_start=date(2026, 4, 6), week_end=date(2026, 4, 12), headlines=["Article One", "Article Two", "Article Three"], categories=["Government", "Government", "Culture"], ) assert os.path.exists(path) img = PILImage.open(path) assert img.format == "JPEG" assert img.size == (480, 800) def test_programmatic_cover_no_headlines(tmp_path): path = generate_programmatic_cover( output_dir=str(tmp_path), week_start=date(2026, 4, 6), week_end=date(2026, 4, 12), headlines=[], categories=[], ) assert os.path.exists(path) img = PILImage.open(path) assert img.size == (480, 800) def test_ai_cover_is_portrait(tmp_path): fake_img = PILImage.new("RGB", (480, 800), color="blue") buf = BytesIO() fake_img.save(buf, format="JPEG") fake_bytes = buf.getvalue() mock_response = MagicMock() mock_response.content = fake_bytes mock_response.raise_for_status = MagicMock() with patch("src.cover.requests.get", return_value=mock_response): path = generate_ai_cover( output_dir=str(tmp_path), week_start=date(2026, 4, 6), week_end=date(2026, 4, 12), headlines=["Test Headline"], categories=["Government"], ) assert os.path.exists(path) img = PILImage.open(path) assert img.format == "JPEG" assert img.size == (480, 800) def test_ai_cover_falls_back_to_programmatic(tmp_path): with patch("src.cover.requests.get", side_effect=Exception("API down")): path = generate_ai_cover( output_dir=str(tmp_path), week_start=date(2026, 4, 6), week_end=date(2026, 4, 12), headlines=["Test"], categories=["Government"], ) assert os.path.exists(path) img = PILImage.open(path) assert img.format == "JPEG" assert img.size == (480, 800) def test_generate_cover_dispatches(tmp_path): with patch("src.cover.generate_ai_cover") as mock_ai: mock_ai.return_value = "/fake/ai.jpg" result = generate_cover("ai", str(tmp_path), date(2026, 4, 6), date(2026, 4, 12), ["A"], ["Government"]) assert result == "/fake/ai.jpg" mock_ai.assert_called_once() with patch("src.cover.generate_programmatic_cover") as mock_prog: mock_prog.return_value = "/fake/text.jpg" result = generate_cover("text", str(tmp_path), date(2026, 4, 6), date(2026, 4, 12), ["A"], ["Government"]) assert result == "/fake/text.jpg" mock_prog.assert_called_once() def test_get_dominant_category(): assert _get_dominant_category(["Government", "Government", "Culture"]) == "Government" assert _get_dominant_category(["Culture", "Government"]) == "Culture" assert _get_dominant_category([]) == "Default" def test_category_prompts_include_massachusetts(): for category, prompt in CATEGORY_PROMPTS.items(): assert "Massachusetts" in prompt, f"Prompt for {category} missing 'Massachusetts'" def test_truncate_to_width_respects_pixel_budget(): font = _get_font(18) short = "Short text" result = _truncate_to_width(short, font, 400) assert result == short, "Short text should not be truncated" long = "A" * 200 result = _truncate_to_width(long, font, 400) assert result.endswith("\u2026"), "Long text should end with ellipsis" assert len(result) < len(long), "Truncated text should be shorter" def test_truncate_to_width_fits_within_budget(): font = _get_font(18) long = "This is a really long headline that should definitely be truncated to fit" max_width = 448 result = _truncate_to_width(long, font, max_width) dummy = PILImage.new("RGBA", (1, 1)) draw = ImageDraw.Draw(dummy) bbox = draw.textbbox((0, 0), result, font=font) rendered_width = bbox[2] - bbox[0] assert rendered_width <= max_width, f"Rendered width {rendered_width} exceeds {max_width}"