"""이미지 API 테스트."""

import io
import shutil
import uuid
from datetime import datetime, timezone
from pathlib import Path

import pytest
from fastapi.testclient import TestClient
from fpdf import FPDF

from app.core.config import settings
from app.core.database import Base, engine, SessionLocal
from app.main import app
from app.models.project import ImageAsset, ProjectSettings, SceneStill
from tests._safety_guards import safe_drop_all, safe_rmtree


def make_test_pdf(text="Test screenplay content") -> bytes:
    pdf = FPDF()
    pdf.add_page()
    pdf.set_font("Helvetica", size=12)
    pdf.cell(200, 10, text=text)
    return pdf.output()


@pytest.fixture(autouse=True)
def _setup_db():
    Base.metadata.create_all(engine)
    with TestClient(app):
        pass
    yield
    safe_drop_all(engine, Base.metadata)
    proj_dir = Path(settings.projects_dir)
    if proj_dir.exists():
        safe_rmtree(proj_dir)


@pytest.fixture()
def client():
    with TestClient(app, raise_server_exceptions=False) as c:
        yield c


def _admin_login(client: TestClient):
    resp = client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin123"})
    assert resp.status_code == 200


def _create_project(client: TestClient) -> str:
    _admin_login(client)
    resp = client.post("/api/v1/projects/", json={"name": "Image Test Proj"})
    assert resp.status_code == 200
    return resp.json()["id"]


def _create_episode(client: TestClient, project_id: str) -> str:
    pdf_bytes = make_test_pdf("INT. OFFICE - DAY")
    resp = client.post(
        f"/api/v1/projects/{project_id}/episodes/",
        data={"episode_number": "1", "title": "Pilot"},
        files={"file": ("pilot.pdf", io.BytesIO(pdf_bytes), "application/pdf")},
    )
    assert resp.status_code == 200
    return resp.json()["id"]


def _insert_image_asset(project_id: str, **overrides) -> str:
    """Directly insert an ImageAsset into the DB for testing."""
    db = SessionLocal()
    try:
        image_id = overrides.pop("id", str(uuid.uuid4()))
        now = datetime.now(timezone.utc).isoformat()
        defaults = {
            "id": image_id,
            "project_id": project_id,
            "asset_type": "reference",
            "entity_id": None,
            "still_id": None,
            "episode_id": None,
            "file_path": "test_image.png",
            "prompt_used": "test prompt",
            "generation_model": "test-model",
            "width": 800,
            "height": 600,
            "status": "generated",
            "review_notes": "",
            "created_at": now,
        }
        defaults.update(overrides)
        asset = ImageAsset(**defaults)
        db.add(asset)
        db.commit()
        return defaults["id"]
    finally:
        db.close()


def test_list_images_empty(client: TestClient):
    """List images returns empty list initially."""
    project_id = _create_project(client)
    resp = client.get(f"/api/v1/projects/{project_id}/images")
    assert resp.status_code == 200
    assert resp.json() == []


def test_list_images_with_filter(client: TestClient):
    """List images filters by asset type."""
    project_id = _create_project(client)
    _insert_image_asset(project_id, asset_type="reference")
    _insert_image_asset(project_id, asset_type="scene")

    # All images
    resp = client.get(f"/api/v1/projects/{project_id}/images")
    assert resp.status_code == 200
    assert len(resp.json()) == 2

    # Filter by type
    resp = client.get(f"/api/v1/projects/{project_id}/images?type=reference")
    assert resp.status_code == 200
    assert len(resp.json()) == 1
    assert resp.json()[0]["asset_type"] == "reference"


def test_image_review_update(client: TestClient):
    """Update review status of an image."""
    project_id = _create_project(client)
    image_id = _insert_image_asset(project_id)

    resp = client.patch(
        f"/api/v1/projects/{project_id}/images/{image_id}/review",
        json={"status": "approved", "notes": "Looks good"},
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["status"] == "approved"
    assert data["review_notes"] == "Looks good"


def test_image_review_invalid_status(client: TestClient):
    """Review with invalid status returns error."""
    project_id = _create_project(client)
    image_id = _insert_image_asset(project_id)

    resp = client.patch(
        f"/api/v1/projects/{project_id}/images/{image_id}/review",
        json={"status": "invalid_status", "notes": ""},
    )
    assert resp.status_code == 400
    assert resp.json()["error"]["code"] == "image.invalid_review_status"


def test_generate_images_no_gemini_key(client: TestClient, monkeypatch):
    """Gemini key 없으면 gate 검증 이전에 image.gemini_key_missing 반환.

    _create_episode()는 fulltext는 채우지만 status='uploaded'를 남기므로,
    만약 route가 key 체크보다 gate를 먼저 돌리면 gate.analysis_incomplete가 나온다.
    이 테스트는 endpoint 순서(fulltext → gemini_key_missing → gate) 중
    두 번째 단계(key check)가 gate보다 우선함을 검증한다.
    env 의존 제거 위해 endpoint 모듈의 settings + gemini_key_count 모두 patch.
    """
    import app.api.v1.images as images_mod

    project_id = _create_project(client)
    episode_id = _create_episode(client, project_id)

    # endpoint 내부 referenced name 직접 교체 (env 의존 제거)
    monkeypatch.setattr(images_mod.settings, "gemini_api_key", "")
    monkeypatch.setattr(images_mod, "gemini_key_count", lambda: 0)

    resp = client.post(
        f"/api/v1/projects/{project_id}/episodes/{episode_id}/generate-images"
    )
    assert resp.status_code == 400
    assert resp.json()["error"]["code"] == "image.gemini_key_missing"


def test_regenerate_needs_fix_none_marked(client: TestClient):
    """Regenerate needs_fix when none are marked returns error."""
    project_id = _create_project(client)
    episode_id = _create_episode(client, project_id)

    resp = client.post(
        f"/api/v1/projects/{project_id}/episodes/{episode_id}/regenerate-needs-fix"
    )
    assert resp.status_code == 400
    assert resp.json()["error"]["code"] == "image.no_needs_fix"


def test_get_image_detail(client: TestClient):
    """Get a single image detail."""
    project_id = _create_project(client)
    image_id = _insert_image_asset(project_id, asset_type="scene", width=1920, height=1080)

    resp = client.get(f"/api/v1/projects/{project_id}/images/{image_id}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["id"] == image_id
    assert data["asset_type"] == "scene"
    assert data["width"] == 1920
    assert data["height"] == 1080


def test_get_image_not_found(client: TestClient):
    """Get nonexistent image returns 404."""
    project_id = _create_project(client)
    resp = client.get(f"/api/v1/projects/{project_id}/images/nonexistent-id")
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == "image.not_found"


def test_image_file_not_found(client: TestClient):
    """Serve image file that doesn't exist on disk returns 404."""
    project_id = _create_project(client)
    # Use a path inside projects_dir so the traversal guard passes
    nonexistent_path = str(Path(settings.projects_dir) / project_id / "nonexistent_image.png")
    image_id = _insert_image_asset(project_id, file_path=nonexistent_path)

    resp = client.get(f"/api/v1/projects/{project_id}/images/{image_id}/file")
    assert resp.status_code == 404


def test_image_file_serve(client: TestClient):
    """Serve an actual image file."""
    import struct
    import zlib

    def make_minimal_png() -> bytes:
        """Create a minimal valid 1x1 white PNG."""
        signature = b'\x89PNG\r\n\x1a\n'

        def chunk(chunk_type, data):
            c = chunk_type + data
            crc = struct.pack('>I', zlib.crc32(c) & 0xffffffff)
            return struct.pack('>I', len(data)) + c + crc

        ihdr_data = struct.pack('>IIBBBBB', 1, 1, 8, 2, 0, 0, 0)
        raw_data = b'\x00\xff\xff\xff'
        idat_data = zlib.compress(raw_data)

        return signature + chunk(b'IHDR', ihdr_data) + chunk(b'IDAT', idat_data) + chunk(b'IEND', b'')

    project_id = _create_project(client)
    # Place test image inside the project directory (within projects_dir)
    proj_dir = Path(settings.projects_dir) / project_id / "assets" / "generated"
    proj_dir.mkdir(parents=True, exist_ok=True)
    png_path = proj_dir / "test_image.png"
    png_path.write_bytes(make_minimal_png())

    image_id = _insert_image_asset(project_id, file_path=str(png_path))

    resp = client.get(f"/api/v1/projects/{project_id}/images/{image_id}/file")
    assert resp.status_code == 200
    assert resp.headers["content-type"] == "image/png"
    assert len(resp.content) > 0


def test_regenerate_single_image(client: TestClient):
    """Regenerate a single image marks it as regenerating."""
    project_id = _create_project(client)
    image_id = _insert_image_asset(project_id)

    resp = client.post(f"/api/v1/projects/{project_id}/images/{image_id}/regenerate")
    assert resp.status_code == 200
    assert resp.json()["ok"] is True

    # Verify status changed
    resp = client.get(f"/api/v1/projects/{project_id}/images/{image_id}")
    assert resp.status_code == 200
    assert resp.json()["status"] == "regenerating"


def test_image_response_includes_sanitization_fields(client: TestClient):
    """ImageResponse includes sanitization_strategy, original_prompt, sanitization_note."""
    project_id = _create_project(client)

    # Image without sanitization (original generation)
    image_id = _insert_image_asset(project_id)
    resp = client.get(f"/api/v1/projects/{project_id}/images/{image_id}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["sanitization_strategy"] is None
    assert data["original_prompt"] is None
    assert data["sanitization_note"] is None

    # Image with sanitization strategy
    image_id2 = _insert_image_asset(
        project_id,
        sanitization_strategy="film_previs",
        original_prompt="original violent prompt",
        sanitization_note="Removed violence, added previs framing",
    )
    resp = client.get(f"/api/v1/projects/{project_id}/images/{image_id2}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["sanitization_strategy"] == "film_previs"
    assert data["original_prompt"] == "original violent prompt"
    assert data["sanitization_note"] == "Removed violence, added previs framing"


def test_list_images_includes_sanitization_fields(client: TestClient):
    """List images includes sanitization fields."""
    project_id = _create_project(client)
    _insert_image_asset(
        project_id,
        sanitization_strategy="movie_poster",
        original_prompt="blocked prompt",
        sanitization_note="Reframed as poster",
    )

    resp = client.get(f"/api/v1/projects/{project_id}/images")
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 1
    assert data[0]["sanitization_strategy"] == "movie_poster"
    assert data[0]["original_prompt"] == "blocked prompt"
    assert data[0]["sanitization_note"] == "Reframed as poster"


def _insert_scene_still(project_id: str, episode_id: str, **overrides) -> str:
    """Directly insert a SceneStill into the DB for testing."""
    db = SessionLocal()
    try:
        still_id = overrides.pop("id", str(uuid.uuid4()))
        now = datetime.now(timezone.utc).isoformat()
        defaults = {
            "id": still_id,
            "project_id": project_id,
            "episode_id": episode_id,
            "still_index": 1,
            "screenplay_scene_heading": "INT. OFFICE - DAY",
            "beat_title": "Test Beat",
            "still_frame_prompt": "A dark office with a holographic display",
            "camera_json": '{"angle": "eye level", "lens": "35mm"}',
            "lighting_json": '{"type": "neon", "color": "blue"}',
            "visible_entities_json": "[]",
            "status": "pending",
            "created_at": now,
        }
        defaults.update(overrides)
        still = SceneStill(**defaults)
        db.add(still)
        db.commit()
        return defaults["id"]
    finally:
        db.close()


def test_compose_prompts_no_openai_key(client: TestClient):
    """Compose prompts fails when no OpenAI API key is configured."""
    project_id = _create_project(client)
    episode_id = _create_episode(client, project_id)
    still_id = _insert_scene_still(project_id, episode_id)

    original_key = settings.openai_api_key
    settings.openai_api_key = ""
    try:
        resp = client.post(
            f"/api/v1/projects/{project_id}/stills/{still_id}/compose-prompts"
        )
        assert resp.status_code == 400
    finally:
        settings.openai_api_key = original_key


def test_compose_prompts_still_not_found(client: TestClient):
    """Compose prompts returns 404 for non-existent still."""
    project_id = _create_project(client)
    resp = client.post(
        f"/api/v1/projects/{project_id}/stills/nonexistent/compose-prompts"
    )
    assert resp.status_code == 400 or resp.status_code == 404


def test_image_response_includes_lineage_fields(client: TestClient):
    """ImageResponse includes prompt_type, code_version, prompt_file_version, reference_image_ids."""
    project_id = _create_project(client)
    image_id = _insert_image_asset(
        project_id,
        prompt_type="cinematic",
        code_version="1.2.0",
        prompt_file_version="t2i_composer/v1",
        reference_image_ids='["img-1", "img-2"]',
    )
    resp = client.get(f"/api/v1/projects/{project_id}/images/{image_id}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["prompt_type"] == "cinematic"
    assert data["code_version"] == "1.2.0"
    assert data["prompt_file_version"] == "t2i_composer/v1"
    assert data["reference_image_ids"] == '["img-1", "img-2"]'


def test_image_response_lineage_defaults(client: TestClient):
    """ImageResponse shows None/default for lineage fields when not set."""
    project_id = _create_project(client)
    image_id = _insert_image_asset(project_id)
    resp = client.get(f"/api/v1/projects/{project_id}/images/{image_id}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["prompt_type"] is None
    assert data["code_version"] is None
    assert data["prompt_file_version"] is None
    assert data["reference_image_ids"] == "[]"


def test_get_composer_prompt_empty(client: TestClient):
    """Get composer prompt returns null values when no override is set."""
    project_id = _create_project(client)
    resp = client.get(f"/api/v1/projects/{project_id}/settings/composer-prompt")
    assert resp.status_code == 200
    data = resp.json()
    assert data["composer_system_prompt"] is None
    assert data["composer_user_prompt"] is None


def test_update_and_get_composer_prompt(client: TestClient):
    """Update then get composer prompt round-trip."""
    project_id = _create_project(client)

    resp = client.patch(
        f"/api/v1/projects/{project_id}/settings/composer-prompt",
        json={
            "composer_system_prompt": "Custom system prompt",
            "composer_user_prompt": "Custom user prompt: {scene_description}",
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["composer_system_prompt"] == "Custom system prompt"
    assert data["composer_user_prompt"] == "Custom user prompt: {scene_description}"

    # Get again
    resp = client.get(f"/api/v1/projects/{project_id}/settings/composer-prompt")
    assert resp.status_code == 200
    data = resp.json()
    assert data["composer_system_prompt"] == "Custom system prompt"
    assert data["composer_user_prompt"] == "Custom user prompt: {scene_description}"


def test_update_composer_prompt_partial(client: TestClient):
    """Partial update of composer prompt -- only update system prompt."""
    project_id = _create_project(client)

    resp = client.patch(
        f"/api/v1/projects/{project_id}/settings/composer-prompt",
        json={"composer_system_prompt": "Only system"},
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["composer_system_prompt"] == "Only system"
    assert data["composer_user_prompt"] is None

    # Update only user prompt now
    resp = client.patch(
        f"/api/v1/projects/{project_id}/settings/composer-prompt",
        json={"composer_user_prompt": "Only user"},
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["composer_system_prompt"] == "Only system"  # should persist
    assert data["composer_user_prompt"] == "Only user"


def test_scene_still_response_includes_t2i_fields(client: TestClient):
    """SceneStillResponse includes t2i_prompt_cinematic, t2i_prompt_closeup, t2i_composer_version."""
    project_id = _create_project(client)
    episode_id = _create_episode(client, project_id)
    still_id = _insert_scene_still(
        project_id,
        episode_id,
        t2i_prompt_cinematic="Wide shot of the office...",
        t2i_prompt_closeup="Close-up of the character...",
        t2i_composer_version="v1",
    )

    resp = client.get(f"/api/v1/projects/{project_id}/episodes/{episode_id}/stills")
    assert resp.status_code == 200
    payload = resp.json()
    stills = payload["stills"]
    assert len(stills) >= 1
    still_data = [s for s in stills if s["id"] == still_id][0]
    assert still_data["t2i_prompt_cinematic"] == "Wide shot of the office..."
    assert still_data["t2i_prompt_closeup"] == "Close-up of the character..."
    assert still_data["t2i_composer_version"] == "v1"


def test_update_still_t2i_prompts(client: TestClient):
    """Update a still's T2I prompts via PATCH."""
    project_id = _create_project(client)
    episode_id = _create_episode(client, project_id)
    still_id = _insert_scene_still(project_id, episode_id)

    resp = client.patch(
        f"/api/v1/projects/{project_id}/stills/{still_id}",
        json={
            "t2i_prompt_cinematic": "Updated cinematic prompt",
            "t2i_prompt_closeup": "Updated closeup prompt",
        },
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["t2i_prompt_cinematic"] == "Updated cinematic prompt"
    assert data["t2i_prompt_closeup"] == "Updated closeup prompt"
