defmodule SpazioSolazzo.BookingSystem.BookingTest do
  use ExUnit.Case, async: true

  use SpazioSolazzo.DataCase
  alias SpazioSolazzo.BookingSystem

  setup do
    {:ok, space} =
      BookingSystem.create_space(
        "Test Space",
        "test-space",
        "Test description",
        2
      )

    user = register_user("testuser@example.com", "Test User")

    date = Date.add(Date.utc_today(), 7)

    %{space: space, user: user, date: date}
  end

  describe "create_booking" do
    test "creates a booking request successfully", %{space: space, date: date} do
      assert {:ok, booking} =
               BookingSystem.create_booking(
                 space.id,
                 nil,
                 date,
                 ~T[09:00:00],
                 ~T[10:00:00],
                 "John Doe",
                 "john@example.com",
                 "+39 1234567890",
                 "Test booking"
               )

      assert booking.space_id == space.id
      assert booking.user_id == nil
      assert booking.date == date
      assert booking.start_time == ~T[09:00:00]
      assert booking.end_time == ~T[10:00:00]
      assert booking.customer_name == "John Doe"
      assert booking.customer_email == "john@example.com"
      assert booking.customer_phone == "+39 1234567890"
      assert booking.customer_comment == "Test booking"
      assert booking.state == :requested
    end

    test "creates booking with authenticated user", %{space: space, user: user, date: date} do
      assert {:ok, booking} =
               BookingSystem.create_booking(
                 space.id,
                 user.id,
                 date,
                 ~T[09:00:00],
                 ~T[10:00:00],
                 "John Doe",
                 user.email,
                 "+39 1234567890",
                 ""
               )

      assert booking.user_id == user.id
      assert to_string(booking.customer_email) == to_string(user.email)
    end

    test "rejects booking with end time before start time", %{space: space, date: date} do
      assert {:error, error} =
               BookingSystem.create_booking(
                 space.id,
                 nil,
                 date,
                 ~T[10:00:00],
                 ~T[09:00:00],
                 "John Doe",
                 "john@example.com",
                 nil,
                 nil
               )

      error_messages = Ash.Error.error_descriptions(error)
      assert String.contains?(error_messages, "must be after start_time")
    end

    test "rejects booking in the past", %{space: space} do
      past_date = Date.add(Date.utc_today(), -1)

      assert {:error, error} =
               BookingSystem.create_booking(
                 space.id,
                 nil,
                 past_date,
                 ~T[09:00:00],
                 ~T[10:00:00],
                 "John Doe",
                 "john@example.com",
                 nil,
                 nil
               )

      error_messages = Ash.Error.error_descriptions(error)
      assert String.contains?(error_messages, "cannot be in the past")
    end

    test "allows booking for today", %{space: space} do
      today = Date.utc_today()

      assert {:ok, booking} =
               BookingSystem.create_booking(
                 space.id,
                 nil,
                 today,
                 ~T[09:00:00],
                 ~T[10:00:00],
                 "John Doe",
                 "john@example.com",
                 nil,
                 nil
               )

      assert booking.date == today
    end

    test "requires customer name and email", %{space: space, date: date} do
      assert {:error, _error} =
               BookingSystem.create_booking(
                 space.id,
                 nil,
                 date,
                 ~T[09:00:00],
                 ~T[10:00:00],
                 "",
                 "",
                 "",
                 ""
               )
    end

    test "phone number is optional", %{space: space, date: date} do
      assert {:ok, booking} =
               BookingSystem.create_booking(
                 space.id,
                 nil,
                 date,
                 ~T[09:00:00],
                 ~T[10:00:00],
                 "John Doe",
                 "john@example.com",
                 nil,
                 nil
               )

      assert booking.customer_phone == nil || booking.customer_phone == ""
    end
  end

  describe "approve_booking/1" do
    test "approves a pending booking", %{space: space, date: date} do
      {:ok, booking} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "John Doe",
          "john@example.com",
          nil,
          nil
        )

      assert booking.state == :requested

      {:ok, approved_booking} = BookingSystem.approve_booking(booking.id)

      assert approved_booking.state == :accepted
      assert approved_booking.id == booking.id
    end

    test "cannot approve already approved booking", %{space: space, date: date} do
      {:ok, booking} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "John Doe",
          "john@example.com",
          nil,
          nil
        )

      {:ok, _} = BookingSystem.approve_booking(booking.id)

      assert {:error, error} = BookingSystem.approve_booking(booking.id)
      error_messages = Ash.Error.error_descriptions(error)
      assert String.contains?(error_messages, "no matching transition")
    end

    test "cannot approve cancelled booking", %{space: space, date: date} do
      {:ok, booking} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "John Doe",
          "john@example.com",
          nil,
          nil
        )

      {:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")

      assert {:error, error} = BookingSystem.approve_booking(booking.id)
      error_messages = Ash.Error.error_descriptions(error)
      assert String.contains?(error_messages, "no matching transition")
    end
  end

  describe "cancel_booking/1" do
    test "cancels a pending booking", %{space: space, date: date} do
      {:ok, booking} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "John Doe",
          "john@example.com",
          nil,
          nil
        )

      {:ok, cancelled_booking} = BookingSystem.cancel_booking(booking.id, "Test cancellation")

      assert cancelled_booking.state == :cancelled
      assert cancelled_booking.id == booking.id
    end

    test "cancels an approved booking", %{space: space, date: date} do
      {:ok, booking} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "John Doe",
          "john@example.com",
          nil,
          nil
        )

      {:ok, _} = BookingSystem.approve_booking(booking.id)
      {:ok, cancelled_booking} = BookingSystem.cancel_booking(booking.id, "Test cancellation")

      assert cancelled_booking.state == :cancelled
    end

    test "cannot cancel already cancelled booking", %{space: space, date: date} do
      {:ok, booking} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "John Doe",
          "john@example.com",
          nil,
          nil
        )

      {:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")

      assert {:error, error} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
      error_messages = Ash.Error.error_descriptions(error)
      assert String.contains?(error_messages, "no matching transition")
    end
  end

  describe "search_bookings/5 for accepted bookings" do
    test "returns only approved bookings for specific date", %{space: space, date: date} do
      {:ok, approved1} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 1",
          "user1@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.approve_booking(approved1.id)

      {:ok, approved2} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[10:00:00],
          ~T[11:00:00],
          "User 2",
          "user2@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.approve_booking(approved2.id)

      {:ok, _pending} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[11:00:00],
          ~T[12:00:00],
          "User 3",
          "user3@example.com",
          "",
          ""
        )

      start_datetime = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
      end_datetime = DateTime.new!(Date.add(date, 1), ~T[00:00:00], "Etc/UTC")

      {:ok, bookings} =
        BookingSystem.search_bookings(
          space.id,
          start_datetime,
          end_datetime,
          [:accepted],
          nil
        )

      assert length(bookings) == 2
      assert Enum.all?(bookings, &(&1.state == :accepted))
    end

    test "does not return cancelled bookings", %{space: space, date: date} do
      {:ok, booking} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 1",
          "user1@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.approve_booking(booking.id)
      {:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")

      start_datetime = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
      end_datetime = DateTime.new!(Date.add(date, 1), ~T[00:00:00], "Etc/UTC")

      {:ok, bookings} =
        BookingSystem.search_bookings(
          space.id,
          start_datetime,
          end_datetime,
          [:accepted],
          nil
        )

      assert bookings == []
    end

    test "only returns bookings for specified date", %{space: space, date: date} do
      other_date = Date.add(date, 1)

      {:ok, booking1} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 1",
          "user1@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.approve_booking(booking1.id)

      {:ok, booking2} =
        BookingSystem.create_booking(
          space.id,
          nil,
          other_date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 2",
          "user2@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.approve_booking(booking2.id)

      start_datetime = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
      end_datetime = DateTime.new!(Date.add(date, 1), ~T[00:00:00], "Etc/UTC")

      {:ok, bookings} =
        BookingSystem.search_bookings(
          space.id,
          start_datetime,
          end_datetime,
          [:accepted],
          nil
        )

      assert length(bookings) == 1
      assert hd(bookings).date == date
    end

    test "only returns bookings for specified space", %{space: space, date: date} do
      {:ok, other_space} =
        BookingSystem.create_space(
          "Other Space",
          "other-space",
          "Other description",
          5
        )

      {:ok, booking} =
        BookingSystem.create_booking(
          other_space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 1",
          "user1@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.approve_booking(booking.id)

      start_datetime = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
      end_datetime = DateTime.new!(Date.add(date, 1), ~T[00:00:00], "Etc/UTC")

      {:ok, bookings} =
        BookingSystem.search_bookings(
          space.id,
          start_datetime,
          end_datetime,
          [:accepted],
          nil
        )

      assert bookings == []
    end
  end

  describe "count pending requests" do
    test "returns only pending bookings", %{space: space, date: date} do
      {:ok, _pending1} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 1",
          "user1@example.com",
          "",
          ""
        )

      {:ok, approved} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[10:00:00],
          ~T[11:00:00],
          "User 2",
          "user2@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.approve_booking(approved.id)

      {:ok, cancelled} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[11:00:00],
          ~T[12:00:00],
          "User 3",
          "user3@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.cancel_booking(cancelled.id, "Test cancellation")

      {:ok, count} =
        Ash.count(SpazioSolazzo.BookingSystem.Booking,
          query: [filter: [state: :requested]]
        )

      assert count == 1
    end

    test "returns zero when no pending requests", %{space: space, date: date} do
      {:ok, booking} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 1",
          "user1@example.com",
          "",
          ""
        )

      {:ok, _} = BookingSystem.approve_booking(booking.id)

      {:ok, count} =
        Ash.count(SpazioSolazzo.BookingSystem.Booking,
          query: [filter: [state: :requested]]
        )

      assert count == 0
    end

    test "counts pending requests across multiple spaces", %{space: space, date: date} do
      {:ok, other_space} =
        BookingSystem.create_space(
          "Other Space",
          "other-space-pending",
          "Other description",
          5
        )

      {:ok, _pending1} =
        BookingSystem.create_booking(
          space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 1",
          "user1@example.com",
          "",
          ""
        )

      {:ok, _pending2} =
        BookingSystem.create_booking(
          other_space.id,
          nil,
          date,
          ~T[09:00:00],
          ~T[10:00:00],
          "User 2",
          "user2@example.com",
          "",
          ""
        )

      {:ok, count} =
        Ash.count(SpazioSolazzo.BookingSystem.Booking,
          query: [filter: [state: :requested]]
        )

      assert count == 2
    end
  end

  describe "create_walk_in/7" do
    test "creates a walk-in booking with accepted state", %{space: space} do
      start_datetime = DateTime.utc_now() |> DateTime.add(1, :hour)
      end_datetime = DateTime.add(start_datetime, 2, :hour)

      assert {:ok, booking} =
               BookingSystem.create_walk_in(
                 space.id,
                 start_datetime,
                 end_datetime,
                 "Walk-in Customer",
                 "walkin@example.com",
                 "+39 1234567890"
               )

      assert booking.space_id == space.id
      assert booking.customer_name == "Walk-in Customer"
      assert booking.customer_email == "walkin@example.com"
      assert booking.customer_phone == "+39 1234567890"
      assert booking.state == :accepted
      assert booking.date == DateTime.to_date(start_datetime)
      # Compare times ignoring microseconds
      expected_start = DateTime.to_time(start_datetime)
      expected_end = DateTime.to_time(end_datetime)

      assert booking.start_time.hour == expected_start.hour
      assert booking.start_time.minute == expected_start.minute
      assert booking.start_time.second == expected_start.second

      assert booking.end_time.hour == expected_end.hour
      assert booking.end_time.minute == expected_end.minute
      assert booking.end_time.second == expected_end.second
    end

    test "creates walk-in without optional fields", %{space: space} do
      start_datetime = DateTime.utc_now() |> DateTime.add(1, :hour)
      end_datetime = DateTime.add(start_datetime, 2, :hour)

      assert {:ok, booking} =
               BookingSystem.create_walk_in(
                 space.id,
                 start_datetime,
                 end_datetime,
                 "Walk-in Customer",
                 "walkin@example.com",
                 nil
               )

      assert booking.customer_phone == nil
      assert booking.customer_comment == nil
      assert booking.state == :accepted
    end

    test "rejects walk-in with end datetime before start datetime", %{space: space} do
      start_datetime = DateTime.utc_now() |> DateTime.add(2, :hour)
      end_datetime = DateTime.add(start_datetime, -1, :hour)

      assert {:error, error} =
               BookingSystem.create_walk_in(
                 space.id,
                 start_datetime,
                 end_datetime,
                 "Walk-in Customer",
                 "walkin@example.com",
                 nil
               )

      error_messages = Ash.Error.error_descriptions(error)
      assert String.contains?(error_messages, "must be after start_datetime")
    end

    test "rejects walk-in with end time in the past", %{space: space} do
      start_datetime = DateTime.utc_now() |> DateTime.add(-2, :hour)
      end_datetime = DateTime.utc_now() |> DateTime.add(-1, :hour)

      assert {:error, error} =
               BookingSystem.create_walk_in(
                 space.id,
                 start_datetime,
                 end_datetime,
                 "Walk-in Customer",
                 "walkin@example.com",
                 nil
               )

      error_messages = Ash.Error.error_descriptions(error)
      assert String.contains?(error_messages, "cannot be in the past")
    end

    test "allows walk-in with start time in the past but end time in the future", %{space: space} do
      start_datetime = DateTime.utc_now() |> DateTime.add(-1, :hour)
      end_datetime = DateTime.utc_now() |> DateTime.add(2, :hour)

      assert {:ok, booking} =
               BookingSystem.create_walk_in(
                 space.id,
                 start_datetime,
                 end_datetime,
                 "Walk-in Customer",
                 "walkin@example.com",
                 nil
               )

      assert booking.state == :accepted
      assert booking.customer_name == "Walk-in Customer"
    end

    test "rejects walk-in with invalid email", %{space: space} do
      start_datetime = DateTime.utc_now() |> DateTime.add(1, :hour)
      end_datetime = DateTime.add(start_datetime, 2, :hour)

      assert {:error, error} =
               BookingSystem.create_walk_in(
                 space.id,
                 start_datetime,
                 end_datetime,
                 "Walk-in Customer",
                 "invalid-email",
                 nil
               )

      error_messages = Ash.Error.error_descriptions(error)
      assert String.contains?(error_messages, "must be a valid email")
    end

    test "converts datetime to date and time correctly", %{space: space} do
      date = Date.add(Date.utc_today(), 1)
      start_time = ~T[14:30:00]
      end_time = ~T[16:45:00]

      start_datetime = DateTime.new!(date, start_time, "Etc/UTC")
      end_datetime = DateTime.new!(date, end_time, "Etc/UTC")

      assert {:ok, booking} =
               BookingSystem.create_walk_in(
                 space.id,
                 start_datetime,
                 end_datetime,
                 "Walk-in Customer",
                 "walkin@example.com",
                 nil
               )

      assert booking.date == date
      assert booking.start_time == start_time
      assert booking.end_time == end_time
    end
  end
end
