use crate::helpers::{
    create_default_bag, create_default_gear, create_default_roast, create_default_roaster,
    spawn_app, spawn_app_with_auth,
};
use brewlog::domain::bags::{Bag, BagWithRoast, NewBag, UpdateBag};
use chrono::NaiveDate;

#[tokio::test]
async fn creating_a_bag_returns_a_201_for_valid_data() {
    // Arrange
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;
    let roast = create_default_roast(&app, roaster.id).await;
    let client = reqwest::Client::new();

    let new_bag = NewBag {
        roast_id: roast.id,
        roast_date: Some(NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()),
        amount: 250.0,
        created_at: None,
    };

    // Act
    let response = client
        .post(app.api_url("/bags"))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&new_bag)
        .send()
        .await
        .expect("Failed to execute request");

    // Assert
    assert_eq!(response.status(), 201);

    let bag: Bag = response.json().await.expect("Failed to parse response");
    assert_eq!(bag.roast_id, roast.id);
    assert_eq!(bag.amount, 250.0);
    assert_eq!(bag.remaining, 250.0);
    assert_eq!(
        bag.roast_date,
        Some(NaiveDate::from_ymd_opt(2023, 1, 1).unwrap())
    );
}

#[tokio::test]
async fn creating_a_bag_without_auth_returns_401() {
    // Arrange
    let app = spawn_app().await; // No auth
    let client = reqwest::Client::new();

    // We can't easily create a roast without auth, so we'll just try to create a bag
    // with a dummy ID. The auth check should happen before validation.
    let new_bag = serde_json::json!({
        "roast_id": 123,
        "amount": 250.0
    });

    // Act
    let response = client
        .post(app.api_url("/bags"))
        .json(&new_bag)
        .send()
        .await
        .expect("Failed to execute request");

    // Assert
    assert_eq!(response.status(), 401);
}

#[tokio::test]
async fn listing_bags_returns_200_and_correct_data() {
    // Arrange
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;
    let roast = create_default_roast(&app, roaster.id).await;
    let client = reqwest::Client::new();

    // Create a bag
    let new_bag = NewBag {
        roast_id: roast.id,
        roast_date: None,
        amount: 500.0,
        created_at: None,
    };

    client
        .post(app.api_url("/bags"))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&new_bag)
        .send()
        .await
        .expect("Failed to create bag");

    // Act
    let response = client
        .get(app.api_url(&format!("/bags?roast_id={}", roast.id)))
        .send()
        .await
        .expect("Failed to execute request");

    // Assert
    assert_eq!(response.status(), 200);

    let bags: Vec<BagWithRoast> = response.json().await.expect("Failed to parse response");
    assert_eq!(bags.len(), 1);
    assert_eq!(bags[0].bag.amount, 500.0);
    assert_eq!(bags[0].roast_name, roast.name);
}

#[tokio::test]
async fn getting_a_bag_returns_200_for_valid_id() {
    // Arrange
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;
    let roast = create_default_roast(&app, roaster.id).await;
    let client = reqwest::Client::new();

    let new_bag = NewBag {
        roast_id: roast.id,
        roast_date: None,
        amount: 250.0,
        created_at: None,
    };

    let create_response = client
        .post(app.api_url("/bags"))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&new_bag)
        .send()
        .await
        .expect("Failed to create bag");

    let created_bag: Bag = create_response
        .json()
        .await
        .expect("Failed to parse response");

    // Act
    let response = client
        .get(app.api_url(&format!("/bags/{}", created_bag.id)))
        .send()
        .await
        .expect("Failed to execute request");

    // Assert
    assert_eq!(response.status(), 200);
    let bag: Bag = response.json().await.expect("Failed to parse response");
    assert_eq!(bag.id, created_bag.id);
}

#[tokio::test]
async fn updating_a_bag_returns_200_and_updates_data() {
    // Arrange
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;
    let roast = create_default_roast(&app, roaster.id).await;
    let client = reqwest::Client::new();

    let new_bag = NewBag {
        roast_id: roast.id,
        roast_date: None,
        amount: 250.0,
        created_at: None,
    };

    let create_response = client
        .post(app.api_url("/bags"))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&new_bag)
        .send()
        .await
        .expect("Failed to create bag");

    let created_bag: Bag = create_response
        .json()
        .await
        .expect("Failed to parse response");

    let update_payload = UpdateBag {
        remaining: Some(100.0),
        closed: Some(true),
        finished_at: Some(NaiveDate::from_ymd_opt(2023, 2, 1).unwrap()),
        ..Default::default()
    };

    // Act
    let response = client
        .put(app.api_url(&format!("/bags/{}", created_bag.id)))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&update_payload)
        .send()
        .await
        .expect("Failed to execute request");

    // Assert
    assert_eq!(response.status(), 200);
    let updated_bag: Bag = response.json().await.expect("Failed to parse response");
    assert_eq!(updated_bag.remaining, 100.0);
    assert!(updated_bag.closed);
    assert_eq!(
        updated_bag.finished_at,
        Some(NaiveDate::from_ymd_opt(2023, 2, 1).unwrap())
    );
}

#[tokio::test]
async fn deleting_a_bag_returns_204() {
    // Arrange
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;
    let roast = create_default_roast(&app, roaster.id).await;
    let client = reqwest::Client::new();

    let new_bag = NewBag {
        roast_id: roast.id,
        roast_date: None,
        amount: 250.0,
        created_at: None,
    };

    let create_response = client
        .post(app.api_url("/bags"))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&new_bag)
        .send()
        .await
        .expect("Failed to create bag");

    let created_bag: Bag = create_response
        .json()
        .await
        .expect("Failed to parse response");

    // Act
    let response = client
        .delete(app.api_url(&format!("/bags/{}", created_bag.id)))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .send()
        .await
        .expect("Failed to execute request");

    // Assert
    assert_eq!(response.status(), 204);

    // Verify deletion
    let get_response = client
        .get(app.api_url(&format!("/bags/{}", created_bag.id)))
        .send()
        .await
        .expect("Failed to execute request");

    assert_eq!(get_response.status(), 404);
}

#[tokio::test]
async fn closing_a_bag_automatically_sets_finished_at() {
    // Arrange
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;
    let roast = create_default_roast(&app, roaster.id).await;
    let client = reqwest::Client::new();

    let new_bag = NewBag {
        roast_id: roast.id,
        roast_date: None,
        amount: 250.0,
        created_at: None,
    };

    let create_response = client
        .post(app.api_url("/bags"))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&new_bag)
        .send()
        .await
        .expect("Failed to create bag");

    let created_bag: Bag = create_response
        .json()
        .await
        .expect("Failed to parse response");

    let update_payload = serde_json::json!({
        "closed": true
    });

    // Act
    let response = client
        .put(app.api_url(&format!("/bags/{}", created_bag.id)))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&update_payload)
        .send()
        .await
        .expect("Failed to execute request");

    // Assert
    assert_eq!(response.status(), 200);
    let updated_bag: Bag = response.json().await.expect("Failed to parse response");
    assert!(updated_bag.closed);
    assert!(updated_bag.finished_at.is_some());
    assert_eq!(
        updated_bag.finished_at.unwrap(),
        chrono::Utc::now().date_naive()
    );
}

#[tokio::test]
async fn updating_bag_amount_recomputes_remaining() {
    // Arrange: create a bag and brew from it to consume some coffee
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;
    let roast = create_default_roast(&app, roaster.id).await;
    let bag = create_default_bag(&app, roast.id).await;
    let grinder = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;
    let brewer = create_default_gear(&app, "brewer", "Hario", "V60 02").await;
    let client = reqwest::Client::new();

    // Brew 15g from the 250g bag → remaining should be 235g
    let new_brew = brewlog::domain::brews::NewBrew {
        bag_id: bag.id,
        coffee_weight: 15.0,
        grinder_id: grinder.id,
        grind_setting: 24.0,
        brewer_id: brewer.id,
        filter_paper_id: None,
        water_volume: 250,
        water_temp: 92.0,
        quick_notes: Vec::new(),
        brew_time: None,
        created_at: None,
    };

    client
        .post(app.api_url("/brews"))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&new_brew)
        .send()
        .await
        .expect("Failed to create brew");

    // Act: update the bag amount from 250g to 500g
    let update_payload = UpdateBag {
        amount: Some(500.0),
        ..Default::default()
    };

    let response = client
        .put(app.api_url(&format!("/bags/{}", bag.id)))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&update_payload)
        .send()
        .await
        .expect("Failed to execute request");

    // Assert: remaining should be 500 - 15 = 485 (recomputed from consumed)
    assert_eq!(response.status(), 200);
    let updated_bag: Bag = response.json().await.expect("Failed to parse response");
    assert_eq!(updated_bag.amount, 500.0);
    assert_eq!(updated_bag.remaining, 485.0);
}
