use std::fs;

use bds_core::db::Database;
use bds_core::db::queries::generated_file_hash::get_generated_file_hash;
use bds_core::db::queries::project::insert_project;
use bds_core::model::{Post, PostStatus, Project};
use bds_core::render::{
    GeneratedWriteOutcome, build_calendar_json, build_core_generation_paths, write_generated_file,
    write_generated_file_forced,
};
use tempfile::TempDir;

fn make_project() -> Project {
    Project {
        id: "p1".into(),
        name: "Blog".into(),
        slug: "blog".into(),
        description: None,
        data_path: None,
        is_active: false,
        created_at: 1,
        updated_at: 1,
    }
}

fn make_post(slug: &str, published_at: i64) -> Post {
    Post {
        id: format!("post-{slug}"),
        project_id: "p1".into(),
        title: slug.into(),
        slug: slug.into(),
        excerpt: None,
        content: Some("Body".into()),
        status: PostStatus::Published,
        author: None,
        language: Some("en".into()),
        do_not_translate: false,
        template_slug: None,
        file_path: String::new(),
        checksum: None,
        tags: vec![],
        categories: vec![],
        published_title: None,
        published_content: None,
        published_tags: None,
        published_categories: None,
        published_excerpt: None,
        created_at: published_at,
        updated_at: published_at,
        published_at: Some(published_at),
    }
}

fn setup() -> (Database, TempDir) {
    let db = Database::open_in_memory().unwrap();
    db.migrate().unwrap();
    insert_project(db.conn(), &make_project()).unwrap();
    (db, TempDir::new().unwrap())
}

#[test]
fn generated_write_skips_unchanged_content() {
    let (db, dir) = setup();

    let first = write_generated_file(db.conn(), dir.path(), "p1", "index.html", "hello").unwrap();
    let second = write_generated_file(db.conn(), dir.path(), "p1", "index.html", "hello").unwrap();
    let third = write_generated_file(db.conn(), dir.path(), "p1", "index.html", "changed").unwrap();

    assert_eq!(first, GeneratedWriteOutcome::Written);
    assert_eq!(second, GeneratedWriteOutcome::SkippedUnchanged);
    assert_eq!(third, GeneratedWriteOutcome::Written);

    let stored = get_generated_file_hash(db.conn(), "p1", "index.html").unwrap();
    assert_eq!(stored.relative_path, "index.html");
    assert!(
        fs::read_to_string(dir.path().join("index.html"))
            .unwrap()
            .contains("changed")
    );
}

#[test]
fn generated_write_rewrites_missing_file_even_when_hash_matches() {
    let (db, dir) = setup();

    let first = write_generated_file(db.conn(), dir.path(), "p1", "index.html", "hello").unwrap();
    fs::remove_file(dir.path().join("index.html")).unwrap();
    let second = write_generated_file(db.conn(), dir.path(), "p1", "index.html", "hello").unwrap();

    assert_eq!(first, GeneratedWriteOutcome::Written);
    assert_eq!(second, GeneratedWriteOutcome::Written);
    assert_eq!(
        fs::read_to_string(dir.path().join("index.html")).unwrap(),
        "hello"
    );
}

#[test]
fn generated_write_trusts_the_cached_hash_like_bds2() {
    let (db, dir) = setup();

    let first = write_generated_file(db.conn(), dir.path(), "p1", "index.html", "hello").unwrap();
    fs::write(dir.path().join("index.html"), "tampered").unwrap();
    let second = write_generated_file(db.conn(), dir.path(), "p1", "index.html", "hello").unwrap();

    assert_eq!(first, GeneratedWriteOutcome::Written);
    assert_eq!(second, GeneratedWriteOutcome::SkippedUnchanged);
    assert_eq!(
        fs::read_to_string(dir.path().join("index.html")).unwrap(),
        "tampered"
    );
}

#[test]
fn forced_generated_write_ignores_the_cached_hash() {
    let (db, dir) = setup();

    write_generated_file(db.conn(), dir.path(), "p1", "index.html", "hello").unwrap();
    fs::write(dir.path().join("index.html"), "tampered").unwrap();
    let forced =
        write_generated_file_forced(db.conn(), dir.path(), "p1", "index.html", "hello").unwrap();

    assert_eq!(forced, GeneratedWriteOutcome::Written);
    assert_eq!(
        fs::read_to_string(dir.path().join("index.html")).unwrap(),
        "hello"
    );
}

#[test]
fn core_generation_paths_match_bds2_outputs() {
    let paths = build_core_generation_paths("en", &["en".into(), "de".into(), "fr".into()]);
    assert!(paths.contains(&"index.html".to_string()));
    assert!(paths.contains(&"404.html".to_string()));
    assert!(paths.contains(&"sitemap.xml".to_string()));
    assert!(paths.contains(&"rss.xml".to_string()));
    assert!(!paths.contains(&"feed.xml".to_string()));
    assert!(paths.contains(&"atom.xml".to_string()));
    assert!(paths.contains(&"calendar.json".to_string()));
    assert!(paths.contains(&"de/index.html".to_string()));
    assert!(paths.contains(&"de/404.html".to_string()));
    assert!(!paths.contains(&"de/sitemap.xml".to_string()));
    assert!(paths.contains(&"de/rss.xml".to_string()));
    assert!(!paths.contains(&"de/feed.xml".to_string()));
    assert!(paths.contains(&"de/atom.xml".to_string()));
    assert!(paths.contains(&"fr/index.html".to_string()));
}

#[test]
fn calendar_json_groups_posts_by_year_month_day() {
    let posts = vec![
        make_post("a", 1_710_000_000_000),
        make_post("b", 1_710_000_000_000),
        make_post("c", 1_712_678_400_000),
    ];

    let json = build_calendar_json(&posts).unwrap();
    assert!(!json.contains('\n'));
    let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

    assert_eq!(parsed["years"]["2024"], 3);
    assert_eq!(parsed["months"]["2024-03"], 2);
    assert_eq!(parsed["months"]["2024-04"], 1);
    assert_eq!(parsed["days"]["2024-03-09"], 2);
    assert_eq!(parsed["days"]["2024-04-09"], 1);
}

#[test]
fn calendar_json_uses_created_at_instead_of_published_at() {
    let mut post = make_post("a", 1_710_000_000_000);
    post.published_at = Some(1_712_678_400_000);

    let json = build_calendar_json(&[post]).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

    assert_eq!(parsed["days"]["2024-03-09"], 1);
    assert!(parsed["days"].get("2024-04-09").is_none());
}
