from fastapi import status
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from datetime import datetime, timedelta, timezone  # Add timezone
from pytest_mock import MockerFixture  # Import MockerFixture

from tests.helpers import generators
from modules.calendar.models import CalendarEvent  # Assuming model exists
from tests.conftest import fake


# Helper function to create an event payload
def create_event_payload(start_offset_days=0, end_offset_days=1):
    # Ensure datetimes are timezone-aware (UTC)
    start_time = datetime.now(timezone.utc) + timedelta(days=start_offset_days)
    end_time = datetime.now(timezone.utc) + timedelta(days=end_offset_days)
    return {
        "title": fake.sentence(nb_words=3),
        "description": fake.text(),
        "start": start_time.isoformat().replace("+00:00", "Z"),  # Ensure Z suffix
        "end": end_time.isoformat().replace("+00:00", "Z"),  # Ensure Z suffix
        "all_day": fake.boolean(),
    }


# --- Test Create Event ---


def test_create_event_unauthorized(client: TestClient) -> None:
    """Test creating an event without authentication."""
    payload = create_event_payload()
    response = client.post("/api/calendar/events", json=payload)
    assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_create_event_success(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:
    """Test creating a calendar event successfully."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    payload = create_event_payload()

    # Mock the celery task sending
    mock_send_task = mocker.patch(
        "core.celery_app.celery_app.send_task"
    )  # Corrected patch target

    response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload,
    )
    assert (
        response.status_code == status.HTTP_201_CREATED
    )  # Change expected status to 201
    data = response.json()
    assert data["title"] == payload["title"]
    assert data["description"] == payload["description"]
    # Assert with Z suffix
    assert data["start"] == payload["start"]
    assert data["end"] == payload["end"]
    assert data["all_day"] == payload["all_day"]
    assert "id" in data
    assert data["user_id"] == user.id

    # Verify in DB
    event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == data["id"]).first()
    assert event_in_db is not None
    assert event_in_db.user_id == user.id
    assert event_in_db.title == payload["title"]

    # Assert that the task was called correctly
    mock_send_task.assert_called_once_with(
        "modules.calendar.tasks.schedule_event_notifications", args=[data["id"]]
    )


# --- Test Get Events ---


def test_get_events_unauthorized(client: TestClient) -> None:
    """Test getting events without authentication."""
    response = client.get("/api/calendar/events")
    assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_get_events_success(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test getting all calendar events for a user."""
    user, password = generators.create_user(
        db, username="testuser_get_events"
    )  # Unique username
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    # Create a couple of events for the user
    payload1 = create_event_payload(0, 1)
    create_rsp1 = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload1,
    )
    assert create_rsp1.status_code == status.HTTP_201_CREATED

    payload2 = create_event_payload(2, 3)
    create_rsp2 = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload2,
    )
    assert create_rsp2.status_code == status.HTTP_201_CREATED

    # Create an event for another user (should not be returned)
    other_user, other_password = generators.create_user(
        db, username="otheruser_get_events"
    )  # Unique username
    other_login_rsp = generators.login(db, other_user.username, other_password)
    other_access_token = other_login_rsp["access_token"]
    other_payload = create_event_payload(4, 5)
    create_rsp_other = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {other_access_token}"},
        json=other_payload,
    )
    assert create_rsp_other.status_code == status.HTTP_201_CREATED

    response = client.get(
        "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}
    )
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert len(data) == 2
    assert data[0]["title"] == payload1["title"]
    assert data[1]["title"] == payload2["title"]
    assert data[0]["user_id"] == user.id
    assert data[1]["user_id"] == user.id


def test_get_events_filtered(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test getting filtered calendar events for a user."""
    user, password = generators.create_user(
        db, username="testuser_filter_events"
    )  # Unique username
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    # Create events
    payload1 = create_event_payload(0, 1)  # Today -> Tomorrow
    create_rsp1 = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload1,
    )
    assert create_rsp1.status_code == status.HTTP_201_CREATED

    payload2 = create_event_payload(5, 6)  # In 5 days -> In 6 days
    create_rsp2 = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload2,
    )
    assert create_rsp2.status_code == status.HTTP_201_CREATED

    payload3 = create_event_payload(10, 11)  # In 10 days -> In 11 days
    create_rsp3 = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload3,
    )
    assert create_rsp3.status_code == status.HTTP_201_CREATED

    # Filter for events starting within the next week
    start_filter = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    end_filter = (
        (datetime.now(timezone.utc) + timedelta(days=7))
        .isoformat()
        .replace("+00:00", "Z")
    )

    response = client.get(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        params={"start": start_filter, "end": end_filter},
    )
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert len(data) == 2  # Should get event 1 and 2
    assert data[0]["title"] == payload1["title"]
    assert data[1]["title"] == payload2["title"]

    # Filter for events starting after 8 days
    start_filter_late = (
        (datetime.now(timezone.utc) + timedelta(days=8))
        .isoformat()
        .replace("+00:00", "Z")
    )
    response = client.get(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        params={"start": start_filter_late},
    )
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert len(data) == 1  # Should get event 3
    assert data[0]["title"] == payload3["title"]


# --- Test Get Event By ID ---


def test_get_event_by_id_unauthorized(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test getting a specific event without authentication."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    payload = create_event_payload()

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload,
    )
    assert create_response.status_code == status.HTTP_201_CREATED
    event_id = create_response.json()["id"]

    response = client.get(f"/api/calendar/events/{event_id}")
    assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_get_event_by_id_success(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test getting a specific event successfully."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    payload = create_event_payload()

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload,
    )
    assert create_response.status_code == status.HTTP_201_CREATED
    event_id = create_response.json()["id"]

    response = client.get(
        f"/api/calendar/events/{event_id}",
        headers={"Authorization": f"Bearer {access_token}"},
    )
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert data["id"] == event_id
    assert data["title"] == payload["title"]
    # Assert datetime with Z suffix
    assert data["start"] == payload["start"]
    assert data["end"] == payload["end"]
    assert data["user_id"] == user.id


def test_get_event_by_id_not_found(db: Session, client: TestClient) -> None:
    """Test getting a non-existent event."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    non_existent_id = 99999

    response = client.get(
        f"/api/calendar/events/{non_existent_id}",
        headers={"Authorization": f"Bearer {access_token}"},
    )
    assert response.status_code == status.HTTP_404_NOT_FOUND


def test_get_event_by_id_forbidden(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test getting another user's event."""
    user1, password_user1 = generators.create_user(
        db, username="user1_forbidden_get"
    )  # Unique username
    user2, password_user2 = generators.create_user(
        db, username="user2_forbidden_get"
    )  # Unique username

    # Log in as user1 and create an event
    login_rsp1 = generators.login(db, user1.username, password_user1)
    access_token1 = login_rsp1["access_token"]
    payload = create_event_payload()

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token1}"},
        json=payload,
    )
    assert create_response.status_code == status.HTTP_201_CREATED
    event_id = create_response.json()["id"]

    # Log in as user2 and try to get user1's event
    login_rsp2 = generators.login(db, user2.username, password_user2)
    access_token2 = login_rsp2["access_token"]

    response = client.get(
        f"/api/calendar/events/{event_id}",
        headers={"Authorization": f"Bearer {access_token2}"},
    )
    assert (
        response.status_code == status.HTTP_404_NOT_FOUND
    )  # Service layer returns 404 if user_id doesn't match


# --- Test Update Event ---


def test_update_event_unauthorized(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test updating an event without authentication."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    payload = create_event_payload()

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload,
    )
    assert create_response.status_code == status.HTTP_201_CREATED
    event_id = create_response.json()["id"]
    update_payload = {"title": "Updated Title"}

    response = client.patch(f"/api/calendar/events/{event_id}", json=update_payload)
    assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_update_event_success(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test updating an event successfully."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    payload = create_event_payload()

    # Mock celery task for creation
    mocker.patch(
        "core.celery_app.celery_app.send_task", return_value=None
    )  # Mock for creation

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload,
    )
    assert (
        create_response.status_code == status.HTTP_201_CREATED
    )  # Ensure creation check uses 201
    event_id = create_response.json()["id"]

    update_payload = {
        "title": "Updated Title",
        "description": "Updated description.",
        "all_day": not payload["all_day"],  # Toggle all_day
    }

    # Mock celery task for update (needs separate mock)
    mock_send_task_update = mocker.patch(
        "modules.calendar.service.celery_app.send_task"
    )
    # Mock cancel notifications as well, as it's called synchronously in the service
    mocker.patch("modules.calendar.tasks.cancel_event_notifications")

    response = client.patch(
        f"/api/calendar/events/{event_id}",
        headers={"Authorization": f"Bearer {access_token}"},
        json=update_payload,
    )
    assert response.status_code == status.HTTP_200_OK
    data = response.json()
    assert data["id"] == event_id
    assert data["title"] == update_payload["title"]
    assert data["description"] == update_payload["description"]
    assert data["all_day"] == update_payload["all_day"]
    # Assert datetime with Z suffix
    assert data["start"] == payload["start"]
    assert data["user_id"] == user.id

    # Verify in DB
    event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first()
    assert event_in_db is not None
    assert event_in_db.title == update_payload["title"]
    assert event_in_db.description == update_payload["description"]
    assert event_in_db.all_day == update_payload["all_day"]

    # Assert that the update task was called correctly
    mock_send_task_update.assert_called_once_with(
        "modules.calendar.tasks.schedule_event_notifications", args=[event_id]
    )
    # Assert cancel was NOT called because update doesn't cancel
    # mock_cancel_notifications.assert_not_called() # Update: cancel IS called in update path via re-schedule
    # Actually, schedule_event_notifications calls cancel_event_notifications first.
    # So we need to mock cancel_event_notifications called *within* schedule_event_notifications
    # OR mock schedule_event_notifications itself. Let's stick to mocking send_task.
    # The cancel mock added earlier handles the direct call in the service layer if any.


def test_update_event_not_found(db: Session, client: TestClient) -> None:
    """Test updating a non-existent event."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    non_existent_id = 99999
    update_payload = {"title": "Updated Title"}

    response = client.patch(
        f"/api/calendar/events/{non_existent_id}",
        headers={"Authorization": f"Bearer {access_token}"},
        json=update_payload,
    )
    assert response.status_code == status.HTTP_404_NOT_FOUND


def test_update_event_forbidden(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test updating another user's event."""
    user1, password_user1 = generators.create_user(
        db, username="user1_forbidden_update"
    )  # Unique username
    user2, password_user2 = generators.create_user(
        db, username="user2_forbidden_update"
    )  # Unique username

    # Log in as user1 and create an event
    login_rsp1 = generators.login(db, user1.username, password_user1)
    access_token1 = login_rsp1["access_token"]
    payload = create_event_payload()

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token1}"},
        json=payload,
    )
    assert create_response.status_code == status.HTTP_201_CREATED
    event_id = create_response.json()["id"]

    # Log in as user2 and try to update user1's event
    login_rsp2 = generators.login(db, user2.username, password_user2)
    access_token2 = login_rsp2["access_token"]
    update_payload = {"title": "Updated by User 2"}

    response = client.patch(
        f"/api/calendar/events/{event_id}",
        headers={"Authorization": f"Bearer {access_token2}"},
        json=update_payload,
    )
    assert (
        response.status_code == status.HTTP_404_NOT_FOUND
    )  # Service layer returns 404 if user_id doesn't match


# --- Test Delete Event ---


def test_delete_event_unauthorized(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test deleting an event without authentication."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    payload = create_event_payload()

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload,
    )
    assert create_response.status_code == status.HTTP_201_CREATED
    event_id = create_response.json()["id"]

    response = client.delete(f"/api/calendar/events/{event_id}")
    assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_delete_event_success(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:
    """Test deleting an event successfully."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    payload = create_event_payload()

    # Mock the celery task sending for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token}"},
        json=payload,
    )
    assert (
        create_response.status_code == status.HTTP_201_CREATED
    )  # Ensure creation check uses 201
    event_id = create_response.json()["id"]

    # Verify event exists before delete
    event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first()
    assert event_in_db is not None

    # Mock the cancel_event_notifications function to prevent Redis call
    mock_cancel_notifications = mocker.patch(
        "modules.calendar.service.cancel_event_notifications"  # Target the function as used in service.py
    )

    response = client.delete(
        f"/api/calendar/events/{event_id}",
        headers={"Authorization": f"Bearer {access_token}"},
    )
    assert response.status_code == status.HTTP_204_NO_CONTENT

    # Assert that cancel_event_notifications was called
    mock_cancel_notifications.assert_called_once_with(event_id)

    # Verify event is deleted from DB
    event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first()
    assert event_in_db is None

    # Try getting the deleted event (should be 404)
    get_response = client.get(
        f"/api/calendar/events/{event_id}",
        headers={"Authorization": f"Bearer {access_token}"},
    )
    assert get_response.status_code == status.HTTP_404_NOT_FOUND


def test_delete_event_not_found(db: Session, client: TestClient) -> None:
    """Test deleting a non-existent event."""
    user, password = generators.create_user(db)
    login_rsp = generators.login(db, user.username, password)
    access_token = login_rsp["access_token"]
    non_existent_id = 99999

    response = client.delete(
        f"/api/calendar/events/{non_existent_id}",
        headers={"Authorization": f"Bearer {access_token}"},
    )
    # The service layer raises NotFound, which should result in 404
    assert response.status_code == status.HTTP_404_NOT_FOUND


def test_delete_event_forbidden(
    db: Session, client: TestClient, mocker: MockerFixture
) -> None:  # Add mocker
    """Test deleting another user's event."""
    user1, password_user1 = generators.create_user(
        db, username="user1_forbidden_delete"
    )  # Unique username
    user2, password_user2 = generators.create_user(
        db, username="user2_forbidden_delete"
    )  # Unique username

    # Log in as user1 and create an event
    login_rsp1 = generators.login(db, user1.username, password_user1)
    access_token1 = login_rsp1["access_token"]
    payload = create_event_payload()

    # Mock celery task for creation
    mocker.patch("core.celery_app.celery_app.send_task")

    create_response = client.post(
        "/api/calendar/events",
        headers={"Authorization": f"Bearer {access_token1}"},
        json=payload,
    )
    assert create_response.status_code == status.HTTP_201_CREATED
    event_id = create_response.json()["id"]

    # Log in as user2 and try to delete user1's event
    login_rsp2 = generators.login(db, user2.username, password_user2)
    access_token2 = login_rsp2["access_token"]

    response = client.delete(
        f"/api/calendar/events/{event_id}",
        headers={"Authorization": f"Bearer {access_token2}"},
    )
    # The service layer raises NotFound if user_id doesn't match, resulting in 404
    assert response.status_code == status.HTTP_404_NOT_FOUND

    # Verify event still exists for user1
    event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first()
    assert event_in_db is not None
    assert event_in_db.user_id == user1.id
