defmodule Raxol.UI.Components.Base.LifecycleTest do
  use ExUnit.Case, async: true
  doctest Raxol.UI.Components.Base.Lifecycle

  import Raxol.Test.TestUtils

  import Raxol.UI.Components.Base.Lifecycle,
    only: [mount: 3, render: 2, process_event: 3, unmount: 2]

  # Define a test component that implements the Component behavior
  defmodule TestComponent do
    @behaviour Raxol.UI.Components.Base.Component

    defstruct id: nil,
              value: nil,
              lifecycle_events: [],
              render_count: 0,
              handle_event_count: 0,
              mounted: false

    def new(opts \\ []) do
      %__MODULE__{
        id:
          Keyword.get(
            opts,
            :id,
            "test-component-#{:erlang.unique_integer([:positive])}"
          ),
        value: Keyword.get(opts, :value, ""),
        lifecycle_events: [],
        render_count: 0,
        handle_event_count: 0,
        mounted: false
      }
    end

    @impl true
    def mount(component, _context \\ %{}) do
      updated =
        Map.update!(component, :lifecycle_events, &[{:mount, :called} | &1])

      {updated, []}
    end

    @impl true
    def render(component, _context) do
      updated = Map.update!(component, :render_count, &(&1 + 1))

      updated =
        Map.update!(updated, :lifecycle_events, &[{:render, :called} | &1])

      {updated,
       %{type: :test_component, id: updated.id, attrs: %{value: updated.value}}}
    end

    @impl true
    def handle_event(event, component, _context) do
      updated = Map.update!(component, :handle_event_count, &(&1 + 1))

      updated =
        Map.update!(updated, :lifecycle_events, &[{:handle_event, event} | &1])

      case event do
        %{type: :test, value: value} -> {:update, %{updated | value: value}}
        %{type: :no_change} -> {:handled, updated}
        _ -> :passthrough
      end
    end

    @impl true
    def update(component, props, _context \\ %{}) do
      updated = Map.merge(component, props)

      updated =
        Map.update!(updated, :lifecycle_events, &[{:update, props} | &1])

      updated
    end

    @impl true
    def unmount(component, _context \\ %{}) do
      updated =
        Map.update!(component, :lifecycle_events, &[{:unmount, :called} | &1])

      {updated, []}
    end

    @impl true
    def init(_), do: {:ok, %{}}
  end

  describe "component lifecycle" do
    test "mount adds event to lifecycle" do
      component = TestComponent.new()
      {mounted, _} = mount(component, %{}, %{})

      assert Enum.at(mounted.lifecycle_events, 0) == {:mount, :called}
      assert Map.has_key?(mounted, :render_count)
      assert Map.has_key?(mounted, :lifecycle_events)
    end

    test "unmount adds event to lifecycle" do
      component = TestComponent.new()
      {unmounted, _} = unmount(component, %{})

      assert Enum.at(unmounted.lifecycle_events, 0) == {:unmount, :called}
      assert Map.has_key?(unmounted, :render_count)
      assert Map.has_key?(unmounted, :lifecycle_events)
    end

    test "render increases render count" do
      component = TestComponent.new()
      context = %{theme: test_theme()}

      # First render
      {rendered, _view} = render(component, context)

      # Render should be idempotent - verify by rendering multiple times
      {rendered_element_map, view2} = render(rendered, context)

      # Asserting on the returned element map instead.
      assert %{type: :test_component} = view2
      assert Map.has_key?(rendered_element_map, :render_count)
    end

    test "update correctly merges props" do
      component = TestComponent.new(value: "initial")
      updated = TestComponent.update(component, %{value: "updated"}, %{})

      assert updated.value == "updated"

      assert Enum.at(updated.lifecycle_events, 0) ==
               {:update, %{value: "updated"}}

      assert Map.has_key?(updated, :render_count)
    end
  end

  describe "event handling" do
    test "handle_event with update returns updated component" do
      component = TestComponent.new(value: "initial")
      event = %{type: :test, value: "updated"}

      {:update, updated} = TestComponent.handle_event(event, component, %{})

      assert updated.value == "updated"
      assert updated.handle_event_count == 1
      assert Enum.at(updated.lifecycle_events, 0) == {:handle_event, event}
    end

    test "handle_event with handled returns same component" do
      component = TestComponent.new()
      event = %{type: :no_change}

      {:handled, updated} = TestComponent.handle_event(event, component, %{})

      assert updated.handle_event_count == 1
      assert Enum.at(updated.lifecycle_events, 0) == {:handle_event, event}
    end

    test "handle_event with passthrough returns passthrough" do
      component = TestComponent.new()
      event = %{type: :unknown}

      result = TestComponent.handle_event(event, component, %{})

      assert result == :passthrough
    end

    test "multiple event handling updates counters correctly" do
      component = TestComponent.new(value: "initial")

      # Handle a series of events
      {:update, after_first} =
        TestComponent.handle_event(
          %{type: :test, value: "first"},
          component,
          %{}
        )

      {:handled, after_second} =
        TestComponent.handle_event(%{type: :no_change}, after_first, %{})

      {:update, after_third} =
        TestComponent.handle_event(
          %{type: :test, value: "third"},
          after_second,
          %{}
        )

      # Verify the final state
      assert after_third.value == "third"
      assert after_third.handle_event_count == 3
      assert length(after_third.lifecycle_events) == 3
    end
  end

  describe "complete lifecycle flow" do
    test "full component lifecycle" do
      # Create and mount
      component = TestComponent.new(value: "initial", id: "test-123")
      {mounted, _} = mount(component, %{}, %{})

      # Render
      {rendered, _view} = render(mounted, %{theme: test_theme()})

      # Handle events - Pass the state AFTER render (rendered)
      {:update, after_event} =
        process_event(rendered, %{type: :test, value: "updated"}, %{
          theme: test_theme()
        })

      # Render again after update - Pass the updated state (after_event)
      {re_rendered, _view2} = render(after_event, %{theme: test_theme()})

      # Update props - Use the re_rendered component
      updated = TestComponent.update(re_rendered, %{value: "final"}, %{})

      # Unmount
      {unmounted, _} = unmount(updated, %{})

      # Verify the full history
      events = Enum.reverse(unmounted.lifecycle_events)

      # Debug output
      IO.puts("DEBUG: Events: #{inspect(events)}")

      assert Enum.at(events, 0) == {:mount, :called}
      assert Enum.at(events, 1) == {:render, :called}

      assert Enum.at(events, 2) ==
               {:handle_event, %{type: :test, value: "updated"}}

      assert Enum.at(events, 3) == {:render, :called}
      assert Enum.at(events, 4) == {:update, %{value: "final"}}
      assert Enum.at(events, 5) == {:unmount, :called}

      # Verify final state
      assert unmounted.render_count == 2
      assert unmounted.handle_event_count == 1
      assert unmounted.id == "test-123"
      assert unmounted.value == "final"
    end
  end
end
