from __future__ import annotations

from datetime import timedelta

import pytest
from sqlalchemy import update

from app.database.base import utcnow
from app.database.models import (
    DestinationStatus,
    JobStatus,
    JobType,
    PostStatus,
    ScheduledJob,
)
from app.errors import PublishError
from tests.conftest import bad_request, network_error, retry_after
from tests.helpers import add_channel, create_ready_post


@pytest.mark.asyncio
async def test_multi_channel_publish_keeps_per_destination_results(ctx, gateway):
    first = await add_channel(ctx, -100_501_501, title="First")
    second = await add_channel(ctx, -100_502_502, title="Second", default=False)
    post = await create_ready_post(ctx, channel_ids=[first.id, second.id])
    gateway.send_errors.append(bad_request("chat not found"))

    result = await ctx.publishing.publish(post.id, actor_id=111_000_001)
    assert result.published_count == 1
    assert result.failed_count == 1
    destinations = await ctx.repos.posts.destinations(post.id)
    assert {d.status for d in destinations} == {
        DestinationStatus.PUBLISHED,
        DestinationStatus.FAILED,
    }
    refreshed = await ctx.repos.posts.get(post.id)
    assert refreshed is not None
    assert refreshed.status == PostStatus.FAILED
    assert refreshed.last_error


@pytest.mark.asyncio
async def test_unknown_send_is_not_retried_blindly(ctx, gateway):
    channel = await add_channel(ctx)
    post = await create_ready_post(ctx, channel_ids=[channel.id])
    gateway.send_errors.append(network_error("connection reset after submit"))

    result = await ctx.publishing.publish(post.id, actor_id=111_000_001)
    assert result.published_count == 0
    assert result.failed_count == 1
    destination = (await ctx.repos.posts.destinations(post.id))[0]
    assert destination.status == DestinationStatus.UNKNOWN
    assert gateway.send_attempts == 1

    with pytest.raises(PublishError):
        await ctx.publishing.retry_failed(post.id, 111_000_001)
    assert gateway.send_attempts == 1


@pytest.mark.asyncio
async def test_retry_after_is_honored_then_publish_succeeds(ctx, gateway):
    channel = await add_channel(ctx)
    post = await create_ready_post(ctx, channel_ids=[channel.id])
    gateway.send_errors.append(retry_after(1))

    result = await ctx.publishing.publish(post.id, actor_id=111_000_001)
    assert result.published_count == 1
    assert gateway.send_attempts == 2
    refreshed = await ctx.repos.posts.get(post.id)
    assert refreshed is not None and refreshed.status == PostStatus.PUBLISHED


@pytest.mark.asyncio
async def test_explicit_retry_of_failed_destination_does_not_resend_published(ctx, gateway):
    first = await add_channel(ctx, -100_511_511, title="First")
    second = await add_channel(ctx, -100_512_512, title="Second", default=False)
    post = await create_ready_post(ctx, channel_ids=[first.id, second.id])
    gateway.send_errors.append(bad_request("temporary bad request"))
    await ctx.publishing.publish(post.id, actor_id=111_000_001)
    assert gateway.send_attempts == 2

    result = await ctx.publishing.retry_failed(post.id, 111_000_001)
    assert result.published_count == 2
    assert gateway.send_attempts == 3
    refreshed = await ctx.repos.posts.get(post.id)
    assert refreshed is not None and refreshed.status == PostStatus.PUBLISHED


@pytest.mark.asyncio
async def test_scheduled_publish_job_is_durable_and_runs_from_db(ctx):
    channel = await add_channel(ctx)
    post = await create_ready_post(ctx, channel_ids=[channel.id])
    run_at = utcnow() + timedelta(hours=1)
    job_id = await ctx.posts.schedule(post.id, run_at, 111_000_001)
    job = await ctx.repos.jobs.get(job_id)
    assert job is not None and job.status == JobStatus.PENDING
    assert job.job_type == JobType.PUBLISH_POST

    # Simulate a restart after the scheduled time: the job is still in SQLite.
    async with ctx.db.session() as session:
        await session.execute(
            update(ScheduledJob)
            .where(ScheduledJob.id == job_id)
            .values(run_at=utcnow() - timedelta(seconds=1))
        )
    processed = await ctx.scheduler.run_once(5)
    assert processed == 1
    job = await ctx.repos.jobs.get(job_id)
    assert job is not None and job.status == JobStatus.DONE
    post = await ctx.repos.posts.get(post.id)
    assert post is not None and post.status == PostStatus.PUBLISHED


@pytest.mark.asyncio
async def test_startup_recovery_marks_running_jobs_pending_and_sending_unknown(ctx):
    channel = await add_channel(ctx)
    post = await create_ready_post(ctx, channel_ids=[channel.id])
    job, _ = await ctx.repos.jobs.enqueue(
        job_type=JobType.PUBLISH_POST,
        run_at=utcnow(),
        payload={"post_id": post.id},
        post_id=post.id,
    )
    async with ctx.db.session() as session:
        row = await session.get(ScheduledJob, job.id)
        assert row is not None
        row.status = JobStatus.RUNNING
    destination = (await ctx.repos.posts.destinations(post.id))[0]
    await ctx.repos.posts.update_destination(destination.id, status=DestinationStatus.SENDING)

    assert await ctx.repos.jobs.recover_running() == 1
    assert await ctx.repos.posts.recover_sending() == 1
    job = await ctx.repos.jobs.get(job.id)
    destination = (await ctx.repos.posts.destinations(post.id))[0]
    assert job is not None and job.status == JobStatus.PENDING
    assert destination.status == DestinationStatus.UNKNOWN


@pytest.mark.asyncio
async def test_job_failure_is_recorded_and_retried(ctx):
    # A delete job pointing at a missing delivery is retryable and must not be
    # silently marked DONE.
    job, _ = await ctx.repos.jobs.enqueue(
        job_type=JobType.DELETE_USER_MESSAGE,
        run_at=utcnow() - timedelta(seconds=1),
        payload={"delivery_id": 99999},
        max_attempts=3,
    )
    await ctx.scheduler.run_once(1)
    job = await ctx.repos.jobs.get(job.id)
    assert job is not None
    assert job.status == JobStatus.PENDING
    assert job.attempts == 1
    assert job.last_error
