import os
from datetime import datetime, timedelta, timezone
from io import BytesIO

import pytest

from app import db
from app.models import ScheduledPost
from app.services import scheduled as scheduled_service
from app.services.errors import ValidationError
from app.services.scheduled import (
    create_scheduled_post, delete_scheduled_post, reschedule_post, retry_post, run_due_posts,
)
from tests.conftest import add_scheduled_post


def _file(name="clip.mp4", content=b"fakevideo"):
    from werkzeug.datastructures import FileStorage
    return FileStorage(stream=BytesIO(content), filename=name)


@pytest.fixture
def fresh_token(monkeypatch):
    monkeypatch.setattr(scheduled_service, "ensure_fresh_token", lambda account: "fresh-token")


def test_create_scheduled_post_stores_the_file(app, account, tmp_path):
    media = str(tmp_path / "media")
    os.makedirs(media, exist_ok=True)
    with app.app_context():
        from app.models import Account as Acc
        acc = db.session.get(Acc, account)
        post = create_scheduled_post(
            acc,
            {"title": "cap", "video_file": _file(), "scheduled_at": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(), "mode": "direct", "privacy_level": "SELF_ONLY"},
            media,
        )
        assert post.status == "pending"
        assert post.video_path and os.path.isfile(post.video_path)
        assert post.video_url is None
        assert post.privacy_level == "SELF_ONLY"


def test_create_scheduled_post_requires_future_time(app, account):
    with app.app_context():
        from app.models import Account as Acc
        acc = db.session.get(Acc, account)
        with pytest.raises(ValidationError):
            create_scheduled_post(acc, {"video_url": "https://x/v.mp4", "scheduled_at": "2000-01-01T00:00:00+00:00"}, "/tmp")
        with pytest.raises(ValidationError):
            create_scheduled_post(acc, {"video_url": "https://x/v.mp4"}, "/tmp")


def test_run_due_posts_publishes_and_cleans_up(app, account, tmp_path, monkeypatch, fresh_token):
    media = str(tmp_path / "media")
    os.makedirs(media, exist_ok=True)
    video_path = os.path.join(media, "due-video.mp4")
    with open(video_path, "wb") as f:
        f.write(b"fakevideo")

    with app.app_context():
        from app.models import Account as Acc
        acc = db.session.get(Acc, account)
        post = ScheduledPost(
            account_id=acc.id,
            title="due post",
            privacy_level="SELF_ONLY",
            mode="direct",
            video_path=video_path,
            scheduled_at=datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(minutes=5),
            status=ScheduledPost.STATUS_PENDING,
        )
        db.session.add(post)
        db.session.commit()
        post_id = post.id

        seen = {}
        monkeypatch.setattr(
            scheduled_service, "publish_video_file",
            lambda token, path, title=None, privacy_level=None, mode=None: seen.update(token=token, existed=os.path.isfile(path)) or {"publish_id": "pub_9"},
        )
        monkeypatch.setattr(scheduled_service, "wait_for_publish", lambda token, pid: {"publish_status": "PUBLISH_COMPLETE", "video_id": "v_9"})

        processed = run_due_posts()
        assert processed == 1
        row = db.session.get(ScheduledPost, post_id)
        assert row.status == "published"
        assert row.publish_id == "pub_9"
        assert row.video_id == "v_9"
        assert seen["token"] == "fresh-token"
        assert seen["existed"] is True
        assert not os.path.exists(video_path)


def test_run_due_posts_leaves_future_posts_alone(app, account, monkeypatch, fresh_token):
    post_id = add_scheduled_post(app, account, scheduled_at=datetime.now(timezone.utc) + timedelta(hours=1))
    with app.app_context():
        assert run_due_posts() == 0
        assert db.session.get(ScheduledPost, post_id).status == "pending"


def test_run_due_posts_marks_failure_with_safe_error(app, account, monkeypatch, fresh_token):
    from app.tiktok_api import TikTokAPIError
    post_id = add_scheduled_post(app, account, scheduled_at=datetime.now(timezone.utc) - timedelta(minutes=1))
    monkeypatch.setattr(
        scheduled_service, "init_video_pull",
        lambda token, url, title=None, privacy_level=None, mode=None: (_ for _ in ()).throw(TikTokAPIError(429, "TikTok API rate limit or daily posting cap reached; try again later.")),
    )
    with app.app_context():
        assert run_due_posts() == 1
        row = db.session.get(ScheduledPost, post_id)
        assert row.status == "failed"
        assert "rate limit" in row.error


def test_reschedule_and_retry(app, account):
    post_id = add_scheduled_post(app, account, status="failed")
    with app.app_context():
        row = db.session.get(ScheduledPost, post_id)
        new_time = (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat()
        reschedule_post(row, new_time)
        assert row.status == "pending"
        assert row.error is None
        row.status = "failed"
        db.session.commit()
        retry_post(row)
        assert row.status == "pending"


def test_delete_removes_row_and_file(app, account, tmp_path):
    media = str(tmp_path / "media")
    os.makedirs(media, exist_ok=True)
    with app.app_context():
        from app.models import Account as Acc
        acc = db.session.get(Acc, account)
        post = create_scheduled_post(
            acc,
            {"video_file": _file(), "scheduled_at": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()},
            media,
        )
        path = post.video_path
        assert os.path.isfile(path)
        delete_scheduled_post(post)
        assert not os.path.exists(path)
        assert db.session.get(ScheduledPost, post.id) is None
