"""``app.core.file_paths.resolve_image_path`` 단위 테스트.

PID 0bb48ebf E2E의 false-positive partial이 ``Path(file_path).exists()`` cwd 의존
때문이었음을 회귀 가드. helper는 절대/상대/None/빈 4 시나리오 + 실제 파일
존재 가드(production 패턴 mirror)를 정확히 처리해야 한다.

helper의 root는 ``settings.projects_dir.parent`` (배포 안전성). 테스트는
``app.core.file_paths.PROJECT_ROOT`` module attribute monkeypatch로 root override.
"""
from __future__ import annotations

from pathlib import Path

import pytest

from app.core.config import settings
from app.core.file_paths import resolve_image_path, to_relative_image_path


def test_resolve_absolute_path_passthrough(tmp_path):
    """이미 절대 경로면 그대로 반환 (Path-equal)."""
    abs_path = tmp_path / "image.png"
    abs_path.write_bytes(b"\x89PNG")

    out = resolve_image_path(str(abs_path))
    assert out is not None
    assert out == abs_path
    assert out.is_absolute()
    assert out.exists()


def test_resolve_relative_path_joined_with_project_root():
    """상대 경로면 ``settings.projects_dir.parent`` 기준으로 join."""
    rel = "projects/abc-123/episodes/ep1/images/floor_plan/fp_test.png"
    out = resolve_image_path(rel)
    assert out is not None
    assert out.is_absolute()
    # join 결과가 정확한지 (실재 여부는 별도)
    expected_root = Path(settings.projects_dir).parent
    assert out == expected_root / rel


def test_resolve_none_returns_none():
    assert resolve_image_path(None) is None


def test_resolve_empty_string_returns_none():
    """빈 문자열도 None 반환 — 호출자가 ``if p and p.exists()`` 패턴 사용 가능."""
    assert resolve_image_path("") is None


def test_resolve_path_object_input(tmp_path):
    """str 외에 Path 객체도 받을 수 있어야 한다."""
    abs_path = tmp_path / "x.png"
    abs_path.write_bytes(b"\x89PNG")
    out = resolve_image_path(abs_path)
    assert out == abs_path


def test_relative_path_exists_with_real_project_root(monkeypatch, tmp_path):
    """production reality mirror: 상대 경로로 저장된 row여도 exists() True."""
    # 가상의 PROJECT_ROOT 하위에 파일 생성
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    rel = "projects/p1/img.png"
    abs_path = tmp_path / rel
    abs_path.parent.mkdir(parents=True, exist_ok=True)
    abs_path.write_bytes(b"\x89PNG")

    out = resolve_image_path(rel)
    assert out is not None
    assert out.exists(), "상대 경로도 PROJECT_ROOT join 후 exists True 여야 함"


def test_relative_path_cwd_independent(monkeypatch, tmp_path):
    """cwd가 PROJECT_ROOT가 아니어도 상대 경로 resolve 결과는 같아야 한다.

    이것이 root cause fix의 핵심 보호 시나리오 — 기존 ``Path(rel).exists()`` 는
    cwd가 PROJECT_ROOT가 아니면 False를 반환했다.
    """
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    rel = "projects/p1/img.png"
    (tmp_path / "projects" / "p1").mkdir(parents=True, exist_ok=True)
    (tmp_path / rel).write_bytes(b"\x89PNG")

    # cwd를 일부러 다른 디렉토리로
    other = tmp_path / "elsewhere"
    other.mkdir()
    monkeypatch.chdir(other)

    out = resolve_image_path(rel)
    # 직접 Path(rel)은 cwd 의존 → 다른 디렉토리에서는 False
    assert not Path(rel).exists()
    # helper resolved Path는 절대 경로이므로 True
    assert out is not None
    assert out.exists()


# ---------------------------------------------------------------------------
# to_relative_image_path — 배포 안전성 (절대 경로 → 상대) producer 측 helper
# ---------------------------------------------------------------------------


def test_to_relative_absolute_under_root(monkeypatch, tmp_path):
    """root 하위 절대 경로 → root 기준 상대."""
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    abs_path = tmp_path / "projects" / "abc" / "img.png"
    out = to_relative_image_path(str(abs_path))
    assert out == "projects/abc/img.png"


def test_to_relative_already_relative_passthrough(monkeypatch, tmp_path):
    """이미 상대면 그대로 반환."""
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    out = to_relative_image_path("projects/abc/img.png")
    assert out == "projects/abc/img.png"


def test_to_relative_outside_root_passthrough(monkeypatch, tmp_path):
    """root 밖 절대 경로는 입력 그대로 반환 (test fixture / legacy 호환)."""
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    out = to_relative_image_path("/elsewhere/xyz.png")
    assert out == "/elsewhere/xyz.png"


def test_to_relative_none_returns_empty():
    assert to_relative_image_path(None) == ""
    assert to_relative_image_path("") == ""


def test_to_relative_path_object(monkeypatch, tmp_path):
    """Path 객체도 받을 수 있다."""
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    abs_path = tmp_path / "projects" / "xyz" / "img.png"
    out = to_relative_image_path(abs_path)
    assert out == "projects/xyz/img.png"


def test_to_relative_then_resolve_round_trip(monkeypatch, tmp_path):
    """to_relative + resolve = 원래 절대 경로 (round-trip)."""
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    abs_path = tmp_path / "projects" / "abc" / "img.png"
    abs_path.parent.mkdir(parents=True, exist_ok=True)
    abs_path.write_bytes(b"\x89PNG")

    rel = to_relative_image_path(str(abs_path))
    resolved = resolve_image_path(rel)
    assert resolved == abs_path
    assert resolved.exists()


def test_to_relative_uses_settings_when_no_monkeypatch():
    """기본 runtime은 ``settings.projects_dir.parent``를 root로 사용."""
    expected_root = Path(settings.projects_dir).parent
    abs_path = expected_root / "projects" / "deploy_test" / "img.png"
    out = to_relative_image_path(str(abs_path))
    assert out == "projects/deploy_test/img.png"


# ---------------------------------------------------------------------------
# ImagePathType TypeDecorator — bind/result + edge cases (Codex M2)
# ---------------------------------------------------------------------------


def test_image_path_type_bind_absolute_to_relative(monkeypatch, tmp_path):
    """절대 경로 입력 → DB 저장 시 상대 경로."""
    from app.core.file_paths import ImagePathType
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    ipt = ImagePathType()
    abs_path = str(tmp_path / "projects" / "abc" / "img.png")
    rel = ipt.process_bind_param(abs_path, None)
    assert rel == "projects/abc/img.png"
    assert not rel.startswith("/")


def test_image_path_type_bind_already_relative_passthrough(monkeypatch, tmp_path):
    """이미 상대면 그대로 저장."""
    from app.core.file_paths import ImagePathType
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    ipt = ImagePathType()
    rel_in = "projects/abc/img.png"
    out = ipt.process_bind_param(rel_in, None)
    assert out == rel_in


def test_image_path_type_bind_none_and_empty():
    from app.core.file_paths import ImagePathType
    ipt = ImagePathType()
    assert ipt.process_bind_param(None, None) is None
    assert ipt.process_bind_param("", None) == ""


def test_image_path_type_bind_path_empty_string_normalized():
    """``Path("")`` → ``"."`` 가 되어 의미 없는 row가 되지 않게 빈 문자열로 normalize."""
    from app.core.file_paths import ImagePathType
    ipt = ImagePathType()
    assert ipt.process_bind_param(Path(""), None) == ""
    assert ipt.process_bind_param("   ", None) == ""
    assert ipt.process_bind_param(".", None) == ""


def test_image_path_type_result_relative_to_absolute(monkeypatch, tmp_path):
    """DB read 시 상대 → 절대 string."""
    from app.core.file_paths import ImagePathType
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    ipt = ImagePathType()
    out = ipt.process_result_value("projects/abc/img.png", None)
    assert out is not None
    assert out.startswith("/")
    assert out == str(tmp_path / "projects" / "abc" / "img.png")


def test_image_path_type_result_legacy_absolute_passthrough(monkeypatch, tmp_path):
    """legacy: DB에 절대로 저장된 row → read 시 그대로 절대 (호환)."""
    from app.core.file_paths import ImagePathType
    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    ipt = ImagePathType()
    legacy_abs = "/some/old/path/img.png"
    out = ipt.process_result_value(legacy_abs, None)
    assert out == legacy_abs


def test_image_path_type_result_none_and_empty():
    from app.core.file_paths import ImagePathType
    ipt = ImagePathType()
    assert ipt.process_result_value(None, None) is None
    assert ipt.process_result_value("", None) == ""


def test_image_path_type_round_trip_with_real_db(monkeypatch, tmp_path):
    """ORM INSERT → SELECT round-trip: DB raw=상대, ORM read=절대.

    full Base.metadata 로 in-memory sqlite 부팅 (FK 의존 모두 충족).
    """
    from sqlalchemy import create_engine, text as sql_text
    from sqlalchemy.orm import sessionmaker
    from app.core.database import Base
    from app.models.project import ImageAsset
    # 모든 모델을 import 해 metadata에 등록
    import app.models.catalog  # noqa: F401
    import app.models.project  # noqa: F401
    import uuid

    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)

    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)

    Sess = sessionmaker(bind=engine)
    db = Sess()
    abs_input = str(tmp_path / "projects" / "p1" / "img.png")
    # FK 우회: project_registry row 먼저 insert
    db.execute(sql_text(
        "INSERT INTO project_registry (id, name, created_by, created_at, updated_at) "
        "VALUES ('proj1', 'p', 'u', '2026-01-01', '2026-01-01')"
    ))
    db.commit()

    asset_id = str(uuid.uuid4())
    asset = ImageAsset(
        id=asset_id,
        project_id="proj1",
        asset_type="reference",
        file_path=abs_input,  # 절대 입력 → bind에서 상대화 기대
        status="generated",
        created_at="2026-01-01",
    )
    db.add(asset)
    db.commit()
    db.close()

    # 새 session으로 round-trip read (identity map cache 우회)
    db2 = Sess()
    raw_path = db2.execute(
        sql_text("SELECT file_path FROM image_asset WHERE id=:id"),
        {"id": asset_id},
    ).scalar()
    assert raw_path == "projects/p1/img.png", f"DB raw expected relative, got: {raw_path!r}"

    # ORM read → 절대 환원
    refetched = db2.query(ImageAsset).filter_by(id=asset_id).first()
    assert refetched.file_path == abs_input
    assert refetched.file_path.startswith("/")
    db2.close()


def test_image_response_schema_does_not_leak_absolute(monkeypatch, tmp_path):
    """API leak 가드 (Codex B2): ImageResponse.file_path 직렬화 시 상대만 노출."""
    from app.schemas.image import ImageResponse

    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    abs_path = str(tmp_path / "projects" / "p1" / "img.png")
    resp = ImageResponse(
        id="x",
        asset_type="reference",
        file_path=abs_path,
        status="generated",
        review_notes="",
        created_at="2026-01-01",
    )
    # 인스턴스 필드는 상대 (validator before)
    assert not resp.file_path.startswith("/")
    assert resp.file_path == "projects/p1/img.png"
    # JSON 직렬화도 상대
    payload = resp.model_dump_json()
    assert "/Users/" not in payload
    assert str(tmp_path) not in payload
    assert "projects/p1/img.png" in payload


def test_image_to_dict_does_not_leak_absolute(monkeypatch, tmp_path):
    """services.image_service_helpers.image_to_dict (Codex B2)."""
    from app.services.image_service_helpers import image_to_dict
    from app.models.project import ImageAsset

    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    abs_path = str(tmp_path / "projects" / "p1" / "img.png")

    class _Stub:
        # ImageAsset attribute mirror — Pydantic 없이 dict 생성만 검증
        id = "x"
        asset_type = "reference"
        entity_id = None
        still_id = None
        episode_id = None
        file_path = abs_path
        prompt_used = ""
        generation_model = ""
        width = None
        height = None
        status = "generated"
        review_notes = ""
        validation_score = None
        validation_result = None
        sanitization_strategy = None
        original_prompt = None
        sanitization_note = None
        variant_type = None
        angle_applied = None
        color_applied = None
        source_image_id = None
        is_primary = 0
        prompt_type = None
        code_version = None
        prompt_file_version = None
        reference_image_ids = "[]"
        parent_image_id = None
        shot_index = None
        theme_label = None
        variant_index = 0
        variant_label = "v00"
        t2i_guide = None
        created_at = "2026-01-01"

    out = image_to_dict(_Stub())
    assert out["file_path"] == "projects/p1/img.png"
    assert not out["file_path"].startswith("/")


# ---------------------------------------------------------------------------
# DB CHECK constraint — 절대 경로 raw INSERT 차단 (Phase 3 alembic 005)
# ---------------------------------------------------------------------------


def test_image_asset_check_rejects_absolute_path_via_raw_sql(monkeypatch, tmp_path):
    """raw SQL INSERT 가 절대 경로 file_path 시 IntegrityError 로 거부된다.

    ImagePathType TypeDecorator 가 ORM bind 시 자동 상대화 하지만, raw
    SQL/Core insert/외부 도구 우회 케이스의 마지막 안전망. ``__table_args__``
    의 CheckConstraint 가 ``Base.metadata.create_all`` 시 함께 생성된다.
    """
    from sqlalchemy import create_engine, text as sql_text
    from sqlalchemy.exc import IntegrityError
    from sqlalchemy.orm import sessionmaker
    from app.core.database import Base
    import app.models.catalog  # noqa: F401
    import app.models.project  # noqa: F401
    import uuid

    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)

    Sess = sessionmaker(bind=engine)
    db = Sess()
    db.execute(sql_text(
        "INSERT INTO project_registry (id, name, created_by, created_at, updated_at) "
        "VALUES ('proj1', 'p', 'u', '2026-01-01', '2026-01-01')"
    ))
    db.commit()

    aid = str(uuid.uuid4())
    with pytest.raises(IntegrityError):
        db.execute(sql_text(
            "INSERT INTO image_asset (id, project_id, asset_type, file_path, "
            "status, variant_index, variant_label, created_at) VALUES "
            "(:id, 'proj1', 'reference', '/abs/should/be/blocked.png', "
            "'generated', 0, 'v00', '2026-01-01')"
        ), {"id": aid})
        db.commit()
    db.rollback()
    db.close()


def test_image_asset_check_allows_relative_path_via_raw_sql(monkeypatch, tmp_path):
    """상대 경로 raw INSERT 는 정상 통과."""
    from sqlalchemy import create_engine, text as sql_text
    from sqlalchemy.orm import sessionmaker
    from app.core.database import Base
    import app.models.catalog  # noqa: F401
    import app.models.project  # noqa: F401
    import uuid

    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)

    Sess = sessionmaker(bind=engine)
    db = Sess()
    db.execute(sql_text(
        "INSERT INTO project_registry (id, name, created_by, created_at, updated_at) "
        "VALUES ('proj1', 'p', 'u', '2026-01-01', '2026-01-01')"
    ))
    db.commit()

    aid = str(uuid.uuid4())
    db.execute(sql_text(
        "INSERT INTO image_asset (id, project_id, asset_type, file_path, "
        "status, variant_index, variant_label, created_at) VALUES "
        "(:id, 'proj1', 'reference', 'projects/p1/img.png', "
        "'generated', 0, 'v00', '2026-01-01')"
    ), {"id": aid})
    db.commit()
    raw = db.execute(
        sql_text("SELECT file_path FROM image_asset WHERE id=:id"),
        {"id": aid},
    ).scalar()
    assert raw == "projects/p1/img.png"
    db.close()


def test_image_asset_check_rejects_update_to_absolute(monkeypatch, tmp_path):
    """raw UPDATE 으로 file_path 를 절대 경로로 바꾸려 해도 IntegrityError 거부.

    INSERT 외에 UPDATE 경로도 CHECK 가 발동해야 진짜 마지막 안전망. raw SQL
    UPDATE 는 ImagePathType bind 를 거치지 않아 CHECK 가 마지막 가드.
    """
    from sqlalchemy import create_engine, text as sql_text
    from sqlalchemy.exc import IntegrityError
    from sqlalchemy.orm import sessionmaker
    from app.core.database import Base
    import app.models.catalog  # noqa: F401
    import app.models.project  # noqa: F401
    import uuid

    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)

    Sess = sessionmaker(bind=engine)
    db = Sess()
    db.execute(sql_text(
        "INSERT INTO project_registry (id, name, created_by, created_at, updated_at) "
        "VALUES ('proj1', 'p', 'u', '2026-01-01', '2026-01-01')"
    ))
    db.commit()

    aid = str(uuid.uuid4())
    db.execute(sql_text(
        "INSERT INTO image_asset (id, project_id, asset_type, file_path, "
        "status, variant_index, variant_label, created_at) VALUES "
        "(:id, 'proj1', 'reference', 'projects/p1/img.png', "
        "'generated', 0, 'v00', '2026-01-01')"
    ), {"id": aid})
    db.commit()

    with pytest.raises(IntegrityError):
        db.execute(
            sql_text("UPDATE image_asset SET file_path = '/abs/path.png' WHERE id = :id"),
            {"id": aid},
        )
        db.commit()
    db.rollback()
    db.close()


def test_image_asset_check_allows_empty_file_path(monkeypatch, tmp_path):
    """빈 문자열 file_path 도 허용 (특정 시나리오에서 placeholder row 생성)."""
    from sqlalchemy import create_engine, text as sql_text
    from sqlalchemy.orm import sessionmaker
    from app.core.database import Base
    import app.models.catalog  # noqa: F401
    import app.models.project  # noqa: F401
    import uuid

    monkeypatch.setattr("app.core.file_paths.PROJECT_ROOT", tmp_path)
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)

    Sess = sessionmaker(bind=engine)
    db = Sess()
    db.execute(sql_text(
        "INSERT INTO project_registry (id, name, created_by, created_at, updated_at) "
        "VALUES ('proj1', 'p', 'u', '2026-01-01', '2026-01-01')"
    ))
    db.commit()

    aid = str(uuid.uuid4())
    db.execute(sql_text(
        "INSERT INTO image_asset (id, project_id, asset_type, file_path, "
        "status, variant_index, variant_label, created_at) VALUES "
        "(:id, 'proj1', 'reference', '', "
        "'generated', 0, 'v00', '2026-01-01')"
    ), {"id": aid})
    db.commit()
    db.close()
