"""엔티티 및 씬 스틸 API 테스트."""

import io
import json
import shutil
import uuid
from datetime import datetime, timezone

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

from app.core.config import settings
from app.core.database import Base, engine, SessionLocal
from app.main import app
from app.models.project import (
    Episode,
    EntityCanon,
    EntityAlias,
    EntityEpisodeLink,
    RelationFact,
    RelationParticipant,
    SceneStill,
)
from tests._safety_guards import safe_drop_all, safe_rmtree


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _uid() -> str:
    return str(uuid.uuid4())


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": "Entity Test Proj"})
    assert resp.status_code == 200
    return resp.json()["id"]


def _create_episode_and_entities(client: TestClient):
    """Helper: create project, episode, and insert entities directly into DB."""
    project_id = _create_project(client)

    # Upload an episode
    pdf_bytes = make_test_pdf("INT. OFFICE - DAY\nCharacter enters")
    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
    episode_id = resp.json()["id"]

    # Insert entities directly into DB
    db = SessionLocal()
    now = _now()
    canon1_id = _uid()
    canon2_id = _uid()

    # Character entity
    db.add(EntityCanon(
        id=canon1_id,
        project_id=project_id,
        entity_type="character",
        name="김철수",
        description="주인공, 30대 남성",
        stable_traits=json.dumps({"visual_anchor_traits": ["검은 정장"], "importance": "major"}),
        status="active",
        created_at=now,
        updated_at=now,
    ))
    # Location entity
    db.add(EntityCanon(
        id=canon2_id,
        project_id=project_id,
        entity_type="location",
        name="사무실",
        description="현대식 사무실",
        stable_traits=json.dumps({"kind": "interior"}),
        status="active",
        created_at=now,
        updated_at=now,
    ))
    db.flush()

    # Aliases
    db.add(EntityAlias(id=_uid(), canon_id=canon1_id, alias="철수"))
    db.add(EntityAlias(id=_uid(), canon_id=canon1_id, alias="김과장"))

    # Episode links
    db.add(EntityEpisodeLink(id=_uid(), project_id=project_id, canon_id=canon1_id, episode_id=episode_id, source="extracted"))
    db.add(EntityEpisodeLink(id=_uid(), project_id=project_id, canon_id=canon2_id, episode_id=episode_id, source="extracted"))
    db.flush()

    # Relation
    relation_id = _uid()
    db.add(RelationFact(
        id=relation_id,
        project_id=project_id,
        relation_family="location",
        relation_type="works_at",
        directionality="directed",
        temporal_scope="episode",
        continuity_priority="medium",
        continuity_reason="김철수가 사무실에서 근무",
        created_at=now,
    ))
    db.flush()

    db.add(RelationParticipant(
        id=_uid(), relation_id=relation_id, canon_id=canon1_id,
        participant_role="worker", participant_order=1,
    ))
    db.add(RelationParticipant(
        id=_uid(), relation_id=relation_id, canon_id=canon2_id,
        participant_role="workplace", participant_order=2,
    ))

    # Scene still
    db.add(SceneStill(
        id=_uid(),
        project_id=project_id,
        episode_id=episode_id,
        still_index=1,
        screenplay_scene_heading="INT. OFFICE - DAY",
        beat_title="김철수 입장",
        still_frame_prompt="검은 정장 차림의 김철수가 사무실에 들어선다.",
        camera_json=json.dumps({"shot_size": "Medium Shot", "angle": "Eye Level"}),
        lighting_json=json.dumps({"setup": "Natural lighting"}),
        visible_entities_json=json.dumps([
            {"entity_name": "김철수", "entity_type": "character", "role": "주인공", "prominence": "primary"},
        ]),
        status="pending",
        created_at=now,
    ))

    db.commit()
    db.close()

    return project_id, episode_id, canon1_id, canon2_id


# -- Entity API Tests --

def test_list_entities(client: TestClient):
    project_id, episode_id, canon1_id, canon2_id = _create_episode_and_entities(client)

    resp = client.get(f"/api/v1/projects/{project_id}/entities")
    assert resp.status_code == 200
    entities = resp.json()
    assert len(entities) == 2
    names = {e["name"] for e in entities}
    assert "김철수" in names
    assert "사무실" in names


def test_list_entities_filter_by_type(client: TestClient):
    project_id, _, _, _ = _create_episode_and_entities(client)

    resp = client.get(f"/api/v1/projects/{project_id}/entities?type=character")
    assert resp.status_code == 200
    entities = resp.json()
    assert len(entities) == 1
    assert entities[0]["entity_type"] == "character"
    assert entities[0]["name"] == "김철수"


def test_get_entity_detail(client: TestClient):
    project_id, _, canon1_id, _ = _create_episode_and_entities(client)

    resp = client.get(f"/api/v1/projects/{project_id}/entities/{canon1_id}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["name"] == "김철수"
    assert data["entity_type"] == "character"
    assert "철수" in data["aliases"]
    assert "김과장" in data["aliases"]
    assert data["episode_count"] == 1
    assert len(data["relations"]) == 1
    assert data["relations"][0]["relation_type"] == "works_at"
    # Episode appearances
    assert len(data["episodes"]) == 1
    assert data["episodes"][0]["title"] == "Pilot"
    assert data["episodes"][0]["episode_number"] == 1


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


def test_update_entity(client: TestClient):
    project_id, _, canon1_id, _ = _create_episode_and_entities(client)

    resp = client.patch(
        f"/api/v1/projects/{project_id}/entities/{canon1_id}",
        json={"name": "김철수 (수정)", "description": "수정된 설명"},
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data["name"] == "김철수 (수정)"
    assert data["description"] == "수정된 설명"


def test_update_entity_not_found(client: TestClient):
    project_id = _create_project(client)
    resp = client.patch(
        f"/api/v1/projects/{project_id}/entities/nonexistent-id",
        json={"name": "New Name"},
    )
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == "entity.not_found"


# -- Scene Still API Tests --

def test_list_stills(client: TestClient):
    project_id, episode_id, _, _ = _create_episode_and_entities(client)

    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
    assert stills[0]["still_index"] == 1
    assert stills[0]["beat_title"] == "김철수 입장"
    assert stills[0]["status"] == "pending"
    # response shape contract (Phase 3b)
    assert "available_composites" in payload
    assert "available_outfits" in payload
    assert "entity_sid_map" in payload


def test_list_stills_empty(client: TestClient):
    project_id = _create_project(client)
    # Upload episode without stills
    pdf_bytes = make_test_pdf("Some content")
    resp = client.post(
        f"/api/v1/projects/{project_id}/episodes/",
        data={"episode_number": "1", "title": "No Stills"},
        files={"file": ("ep.pdf", io.BytesIO(pdf_bytes), "application/pdf")},
    )
    episode_id = resp.json()["id"]

    resp = client.get(f"/api/v1/projects/{project_id}/episodes/{episode_id}/stills")
    assert resp.status_code == 200
    payload = resp.json()
    assert payload["stills"] == []
    assert payload["available_composites"] == []
    assert payload["available_outfits"] == []
    assert payload["entity_sid_map"] == {}


# -- Analyze Endpoint Tests --

def test_analyze_no_api_key(client: TestClient):
    """Analyze should fail when no OpenAI API key is set."""
    project_id = _create_project(client)
    pdf_bytes = make_test_pdf("INT. OFFICE - DAY")
    resp = client.post(
        f"/api/v1/projects/{project_id}/episodes/",
        data={"episode_number": "1", "title": "Test"},
        files={"file": ("ep.pdf", io.BytesIO(pdf_bytes), "application/pdf")},
    )
    episode_id = resp.json()["id"]

    # Ensure no API key is set
    original_key = settings.openai_api_key
    settings.openai_api_key = ""
    try:
        resp = client.post(f"/api/v1/projects/{project_id}/episodes/{episode_id}/analyze")
        assert resp.status_code == 400
        assert resp.json()["error"]["code"] == "analysis.openai_key_missing"
    finally:
        settings.openai_api_key = original_key


def test_analyze_episode_not_found(client: TestClient):
    project_id = _create_project(client)
    original_key = settings.openai_api_key
    settings.openai_api_key = "test-key"
    try:
        resp = client.post(f"/api/v1/projects/{project_id}/episodes/nonexistent/analyze")
        assert resp.status_code == 404
        assert resp.json()["error"]["code"] == "episode.not_found"
    finally:
        settings.openai_api_key = original_key


def test_analyze_no_text(client: TestClient):
    """Analyze should fail when episode has no fulltext."""
    project_id = _create_project(client)
    pdf_bytes = make_test_pdf("INT. OFFICE - DAY")
    resp = client.post(
        f"/api/v1/projects/{project_id}/episodes/",
        data={"episode_number": "1", "title": "Test"},
        files={"file": ("ep.pdf", io.BytesIO(pdf_bytes), "application/pdf")},
    )
    episode_id = resp.json()["id"]

    # Manually clear fulltext in the DB
    db = SessionLocal()
    ep = db.query(Episode).filter(Episode.id == episode_id).first()
    ep.fulltext = ""
    db.commit()
    db.close()

    original_key = settings.openai_api_key
    settings.openai_api_key = "test-key"
    try:
        resp = client.post(f"/api/v1/projects/{project_id}/episodes/{episode_id}/analyze")
        assert resp.status_code == 400
        assert resp.json()["error"]["code"] == "analysis.no_text"
    finally:
        settings.openai_api_key = original_key
