//! Pure invariants for first-class background tasks.

use domain::{
    BackgroundTask, BackgroundTaskError, BackgroundTaskKind, BackgroundTaskResult,
    BackgroundTaskState, BackgroundTaskWakePolicy,
};
use uuid::Uuid;

fn task_id(n: u128) -> domain::TaskId {
    domain::TaskId::from_uuid(Uuid::from_u128(n))
}

fn project_id(n: u128) -> domain::ProjectId {
    domain::ProjectId::from_uuid(Uuid::from_u128(n))
}

fn agent_id(n: u128) -> domain::AgentId {
    domain::AgentId::from_uuid(Uuid::from_u128(n))
}

fn command_task() -> BackgroundTask {
    BackgroundTask::new(
        task_id(1),
        project_id(2),
        agent_id(3),
        BackgroundTaskKind::Command {
            label: "build".into(),
        },
        BackgroundTaskWakePolicy::WakeOwner,
        100,
        Some(200),
    )
    .unwrap()
}

fn success_result(at: u64) -> BackgroundTaskResult {
    BackgroundTaskResult::Success {
        finished_at_ms: at,
        exit_code: Some(0),
        summary: "ok".into(),
        stdout_tail: None,
        stderr_tail: None,
    }
}

fn failure_result(at: u64) -> BackgroundTaskResult {
    BackgroundTaskResult::Failure {
        finished_at_ms: at,
        exit_code: Some(1),
        error: "failed".into(),
        stdout_tail: None,
        stderr_tail: None,
    }
}

#[test]
fn new_task_is_queued_without_result_or_delivery() {
    let task = command_task();

    assert_eq!(task.state, BackgroundTaskState::Queued);
    assert_eq!(task.result, None);
    assert!(!task.completion_delivered);
    assert!(!task.is_terminal());
    assert!(!task.has_pending_completion_delivery());
}

#[test]
fn deadline_must_not_precede_creation() {
    let err = BackgroundTask::new(
        task_id(1),
        project_id(2),
        agent_id(3),
        BackgroundTaskKind::Command {
            label: "build".into(),
        },
        BackgroundTaskWakePolicy::WakeOwner,
        100,
        Some(99),
    )
    .unwrap_err();

    assert_eq!(err, BackgroundTaskError::InvalidTimestamps);
}

#[test]
fn labels_must_be_non_empty_and_bounded() {
    let empty = BackgroundTaskKind::Command { label: " ".into() };
    assert_eq!(empty.validate(), Err(BackgroundTaskError::EmptyLabel));

    let oversized = BackgroundTaskKind::Maintenance {
        label: "x".repeat(domain::BACKGROUND_TASK_LABEL_MAX_CHARS + 1),
    };
    assert_eq!(
        oversized.validate(),
        Err(BackgroundTaskError::FieldTooLarge {
            field: "label",
            max: domain::BACKGROUND_TASK_LABEL_MAX_CHARS,
        })
    );
}

#[test]
fn allowed_non_terminal_transitions_update_timestamp() {
    let running = command_task()
        .transition(BackgroundTaskState::Running, 120)
        .unwrap();
    assert_eq!(running.state, BackgroundTaskState::Running);
    assert_eq!(running.updated_at_ms, 120);

    let waiting = running
        .transition(BackgroundTaskState::Waiting, 130)
        .unwrap();
    assert_eq!(waiting.state, BackgroundTaskState::Waiting);

    let running_again = waiting
        .transition(BackgroundTaskState::Running, 140)
        .unwrap();
    assert_eq!(running_again.state, BackgroundTaskState::Running);
}

#[test]
fn illegal_transitions_are_rejected() {
    let task = command_task();
    let err = task
        .transition(BackgroundTaskState::Waiting, 120)
        .unwrap_err();

    assert_eq!(
        err,
        BackgroundTaskError::InvalidTransition {
            from: BackgroundTaskState::Queued,
            to: BackgroundTaskState::Waiting,
        }
    );
}

#[test]
fn lifecycle_timestamps_must_not_regress() {
    let running = command_task()
        .transition(BackgroundTaskState::Running, 120)
        .unwrap();

    assert_eq!(
        running.transition(BackgroundTaskState::Waiting, 119),
        Err(BackgroundTaskError::InvalidTimestamps)
    );
    assert_eq!(
        running.complete(success_result(119)),
        Err(BackgroundTaskError::InvalidTimestamps)
    );
}

#[test]
fn terminal_transition_requires_complete_with_matching_result() {
    let running = command_task()
        .transition(BackgroundTaskState::Running, 120)
        .unwrap();

    assert_eq!(
        running.transition(BackgroundTaskState::Completed, 130),
        Err(BackgroundTaskError::MissingResult)
    );

    let completed = running.complete(success_result(130)).unwrap();
    assert_eq!(completed.state, BackgroundTaskState::Completed);
    assert_eq!(completed.updated_at_ms, 130);
    assert!(completed.is_terminal());
    assert!(completed.has_pending_completion_delivery());
}

#[test]
fn terminal_states_are_immutable() {
    let completed = command_task()
        .transition(BackgroundTaskState::Running, 120)
        .unwrap()
        .complete(success_result(130))
        .unwrap();

    let err = completed
        .transition(BackgroundTaskState::Running, 140)
        .unwrap_err();
    assert_eq!(
        err,
        BackgroundTaskError::InvalidTransition {
            from: BackgroundTaskState::Completed,
            to: BackgroundTaskState::Running,
        }
    );

    let err = completed.complete(failure_result(140)).unwrap_err();
    assert_eq!(
        err,
        BackgroundTaskError::InvalidTransition {
            from: BackgroundTaskState::Completed,
            to: BackgroundTaskState::Failed,
        }
    );
}

#[test]
fn result_variants_determine_terminal_state() {
    let running = command_task()
        .transition(BackgroundTaskState::Running, 120)
        .unwrap();

    assert_eq!(
        running.complete(failure_result(130)).unwrap().state,
        BackgroundTaskState::Failed
    );
    assert_eq!(
        running
            .complete(BackgroundTaskResult::Cancelled {
                finished_at_ms: 130,
                reason: "user".into(),
            })
            .unwrap()
            .state,
        BackgroundTaskState::Cancelled
    );
    assert_eq!(
        running
            .complete(BackgroundTaskResult::Expired {
                finished_at_ms: 130,
                reason: "deadline".into(),
            })
            .unwrap()
            .state,
        BackgroundTaskState::Expired
    );
}

#[test]
fn completion_delivery_only_once_after_terminal_state() {
    let running = command_task()
        .transition(BackgroundTaskState::Running, 120)
        .unwrap();

    assert_eq!(
        running.mark_completion_delivered(),
        Err(BackgroundTaskError::MissingResult)
    );

    let delivered = running
        .complete(success_result(130))
        .unwrap()
        .mark_completion_delivered()
        .unwrap();

    assert!(delivered.completion_delivered);
    assert!(!delivered.has_pending_completion_delivery());
    assert_eq!(
        delivered.mark_completion_delivered(),
        Err(BackgroundTaskError::CompletionAlreadyDelivered)
    );
}

#[test]
fn result_text_fields_are_bounded() {
    let summary_too_large = BackgroundTaskResult::Success {
        finished_at_ms: 1,
        exit_code: None,
        summary: "x".repeat(domain::BACKGROUND_TASK_TEXT_MAX_BYTES + 1),
        stdout_tail: None,
        stderr_tail: None,
    };
    assert_eq!(
        summary_too_large.validate(),
        Err(BackgroundTaskError::FieldTooLarge {
            field: "summary",
            max: domain::BACKGROUND_TASK_TEXT_MAX_BYTES,
        })
    );

    let tail_too_large = BackgroundTaskResult::Failure {
        finished_at_ms: 1,
        exit_code: None,
        error: "failed".into(),
        stdout_tail: Some("x".repeat(domain::BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES + 1)),
        stderr_tail: None,
    };
    assert_eq!(
        tail_too_large.validate(),
        Err(BackgroundTaskError::FieldTooLarge {
            field: "stdout_tail",
            max: domain::BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES,
        })
    );
}

#[test]
fn background_task_serializes_with_camel_case_fields() {
    let task = command_task();
    let json = serde_json::to_string(&task).unwrap();

    assert!(json.contains("\"ownerAgentId\""), "json was {json}");
    assert!(
        json.contains("\"wakePolicy\":\"wakeOwner\""),
        "json was {json}"
    );
    assert!(json.contains("\"completionDelivered\""), "json was {json}");
    assert_eq!(serde_json::from_str::<BackgroundTask>(&json).unwrap(), task);
}
