import pytest

from aceai.agent.session import SessionRecorder, SessionStore
from aceai.core.events import AgentEventBuilder
from aceai.core.models import AgentStep
from aceai.agent.tui.app import AceAITUI
from aceai.agent.tui.demo import static_demo_events
from aceai.agent.tui.events import adapt_agent_event, user_message_event
from aceai.agent.tui.state import initial_state, reduce_events
from aceai.agent.tui.widgets import CommandInput, DetailWidget, StreamWidget, TimelineWidget
from aceai.llm.models import LLMResponse, LLMUsage
from textual.widgets import DataTable, Footer, Static


def test_reduce_events_tracks_run_completion() -> None:
    events = static_demo_events()

    state = reduce_events(events)

    assert state.status == "completed"
    assert state.final_answer == "Static TUI prototype is ready to inspect."
    assert state.error is None
    assert state.selected_event_id == events[-1].event_id


def test_reduce_events_tracks_step_and_tool_state() -> None:
    state = reduce_events(static_demo_events())

    assert len(state.steps) == 1
    step = state.steps[0]
    assert step.status == "completed"
    assert len(step.tools) == 1
    tool_state = step.tools[0]
    assert tool_state.name == "search_docs"
    assert tool_state.status == "completed"
    assert tool_state.arguments == '{"query":"aceai tui"}'
    assert tool_state.output == '{"matches":["spec/tui.md","docs/tui.md"]}'


def test_reduce_events_assigns_global_step_numbers() -> None:
    events = [
        adapt_agent_event(
            AgentEventBuilder(step_index=0, step_id="run-1-step-1").llm_started()
        ),
        adapt_agent_event(
            AgentEventBuilder(step_index=0, step_id="run-2-step-1").llm_started()
        ),
    ]

    state = reduce_events(events)

    assert [step.step_index for step in state.steps] == [0, 1]


def test_reduce_events_does_not_add_user_questions_to_timeline() -> None:
    state = reduce_events([user_message_event("What changed?")])

    assert state.steps == []
    assert state.events[0].kind == "user_message"


def test_reduce_events_does_not_add_session_notices_to_timeline() -> None:
    from aceai.agent.tui.events import session_notice_event

    state = reduce_events([session_notice_event("Sessions")])

    assert state.steps == []
    assert state.events[0].kind == "session_notice"


def test_reduce_events_does_not_add_restored_transcript_to_timeline() -> None:
    from aceai.agent.session import SessionMessage, messages_to_tui_events

    created_at = "2026-05-04T00:00:00+00:00"
    events = messages_to_tui_events(
        [
            SessionMessage(kind="user", content="question", created_at=created_at),
            SessionMessage(kind="assistant", content="answer", created_at=created_at),
            SessionMessage(
                kind="tool",
                content="",
                created_at=created_at,
                tool_name="read_text_file",
                tool_call_id="call-1",
                tool_arguments='{"path":"README.md"}',
                tool_output="contents",
                status="completed",
            ),
        ]
    )

    state = reduce_events(events)

    assert state.steps == []
    assert state.status == "idle"


def test_initial_state_is_idle() -> None:
    state = initial_state()

    assert state.status == "idle"
    assert state.steps == []
    assert state.events == []


def test_reduce_events_tracks_usage_totals() -> None:
    first = AgentEventBuilder(step_index=0, step_id="step-1").llm_completed(
        step=AgentStep(
            llm_response=LLMResponse(
                text="first",
                usage=LLMUsage(
                    input_tokens=100,
                    cached_input_tokens=40,
                    output_tokens=20,
                    total_tokens=120,
                ),
            )
        )
    )
    second = AgentEventBuilder(step_index=1, step_id="step-2").llm_completed(
        step=AgentStep(
            llm_response=LLMResponse(
                text="second",
                usage=LLMUsage(input_tokens=150, output_tokens=30, total_tokens=180),
            )
        )
    )

    state = reduce_events([adapt_agent_event(first), adapt_agent_event(second)])

    assert state.usage.current_context_tokens == 150
    assert state.usage.session_input_tokens == 250
    assert state.usage.session_cached_input_tokens == 40
    assert state.usage.session_output_tokens == 50
    assert state.usage.session_total_tokens == 300


def test_reduce_events_tracks_cost_totals() -> None:
    first = AgentEventBuilder(step_index=0, step_id="step-1").llm_completed(
        step=AgentStep(
            llm_response=LLMResponse(
                text="first",
                model="gpt-5.5",
                usage=LLMUsage(
                    input_tokens=1_000,
                    cached_input_tokens=600,
                    output_tokens=100,
                    total_tokens=1_100,
                ),
            )
        )
    )
    second = AgentEventBuilder(step_index=1, step_id="step-2").llm_completed(
        step=AgentStep(
            llm_response=LLMResponse(
                text="second",
                model="gpt-5.4-mini",
                usage=LLMUsage(input_tokens=1_000, output_tokens=100, total_tokens=1_100),
            )
        )
    )

    state = reduce_events([adapt_agent_event(first), adapt_agent_event(second)])

    assert state.usage.current_cost_usd is not None
    assert state.usage.session_cost_usd is not None
    assert round(state.usage.current_cost_usd, 6) == 0.0012
    assert round(state.usage.session_cost_usd, 6) == 0.0065


def test_reduce_events_keeps_missing_usage_unknown() -> None:
    state = reduce_events(
        [
            adapt_agent_event(
                AgentEventBuilder(step_index=0, step_id="step-1").llm_completed(
                    step=AgentStep(llm_response=LLMResponse(text="answer"))
                )
            )
        ]
    )

    assert state.usage.current_context_tokens is None
    assert state.usage.session_input_tokens is None
    assert state.usage.session_output_tokens is None
    assert state.usage.session_total_tokens is None


@pytest.mark.anyio
async def test_static_tui_loads_fixture_events() -> None:
    events = static_demo_events()
    app = AceAITUI(events)

    async with app.run_test():
        assert app._state.status == "completed"
        timeline = app.query_one(TimelineWidget)
        stream = app.query_one(StreamWidget)
        detail = app.query_one(DetailWidget)
        assert timeline.can_focus
        assert stream.can_focus
        assert detail.can_focus
        assert timeline.has_class("collapsed")
        assert detail.has_class("collapsed")


@pytest.mark.anyio
async def test_stream_scrolls_to_latest_content() -> None:
    builder = AgentEventBuilder(step_index=0, step_id="step-1")
    events = [user_message_event("Show me many lines")]
    for line_number in range(80):
        events.append(
            adapt_agent_event(
                builder.llm_text_delta(text_delta=f"line {line_number}\n")
            )
        )
    app = AceAITUI(events)

    async with app.run_test(size=(80, 20)) as pilot:
        await pilot.pause(0.1)
        stream = app.query_one(StreamWidget)

        assert stream.max_scroll_y > 0
        assert stream.scroll_y == stream.max_scroll_y


@pytest.mark.anyio
async def test_stream_does_not_show_horizontal_scrollbar() -> None:
    builder = AgentEventBuilder(step_index=0, step_id="step-1")
    events = [
        adapt_agent_event(
            builder.llm_text_delta(
                text_delta=(
                    "This is a long assistant response that should wrap inside "
                    "the stream instead of creating a horizontal scrollbar. "
                    * 8
                )
            )
        )
    ]
    app = AceAITUI(events)

    async with app.run_test(size=(60, 20)) as pilot:
        await pilot.pause(0.1)
        stream = app.query_one(StreamWidget)

        assert not stream.show_horizontal_scrollbar
        assert stream.max_scroll_x == 0


@pytest.mark.anyio
async def test_escape_exits_input_mode_and_returns_focus_to_stream() -> None:
    app = AceAITUI([])

    async with app.run_test() as pilot:
        command_input = app.query_one(CommandInput)
        stream = app.query_one(StreamWidget)
        command_input.focus()

        assert command_input.has_focus

        await pilot.press("escape")

        assert not command_input.has_focus
        assert stream.has_focus


@pytest.mark.anyio
async def test_enter_from_main_view_focuses_input() -> None:
    app = AceAITUI([])

    async with app.run_test() as pilot:
        command_input = app.query_one(CommandInput)
        stream = app.query_one(StreamWidget)
        stream.focus()
        await pilot.pause()

        assert stream.has_focus

        await pilot.press("enter")

        assert command_input.has_focus


@pytest.mark.anyio
async def test_input_sits_above_footer_without_overlap() -> None:
    app = AceAITUI([])

    async with app.run_test(size=(80, 20)) as pilot:
        await pilot.pause(0.1)
        command_input = app.query_one(CommandInput)
        footer = app.query_one(Footer)

        assert command_input.region.y + command_input.region.height <= footer.region.y


@pytest.mark.anyio
async def test_tui_header_uses_session_id(tmp_path) -> None:
    store = SessionStore(tmp_path)
    metadata = store.create_session()
    app = AceAITUI(
        [],
        session_recorder=SessionRecorder(store, metadata.session_id),
        session_id=metadata.session_id,
    )

    async with app.run_test():
        assert app.title == f"AceAI {metadata.session_id}"


@pytest.mark.anyio
async def test_tui_can_show_and_switch_sessions(tmp_path) -> None:
    store = SessionStore(tmp_path)
    first = store.create_session()
    second = store.create_session()
    SessionRecorder(store, first.session_id).record(user_message_event("first question"))
    SessionRecorder(store, second.session_id).record(user_message_event("second question"))
    app = AceAITUI(
        store.load_tui_events(first.session_id),
        session_recorder=SessionRecorder(store, first.session_id),
        session_id=first.session_id,
    )

    async with app.run_test():
        app.show_sessions()

        assert app._state.events[-1].kind == "session_notice"
        assert "Total cost: $0.000000" in app._state.events[-1].content
        assert first.session_id in app._state.events[-1].content
        assert second.session_id in app._state.events[-1].content

        app.switch_session(second.session_id)

        assert app.title == f"AceAI {second.session_id}"
        assert app._state.events[0].content == "second question"


@pytest.mark.anyio
async def test_session_selector_uses_table_columns(tmp_path) -> None:
    store = SessionStore(tmp_path)
    first = store.create_session()
    second = store.create_session()
    store.update_session_title(first.session_id, "first question - 2026-05-04 12:13:14")
    store.update_session_title(second.session_id, "second question")
    app = AceAITUI(
        store.load_tui_events(first.session_id),
        session_recorder=SessionRecorder(store, first.session_id),
        session_id=first.session_id,
    )

    async with app.run_test() as pilot:
        app.open_session_selector()
        await pilot.pause(0.1)

        table = app.screen.query_one("#session-table", DataTable)

        assert [column.label.plain for column in table.ordered_columns] == [
            "Current",
            "Title",
            "Updated",
            "Created",
            "Session ID",
        ]
        assert table.row_count == 2
        assert table.ordered_rows[table.cursor_row].key.value == first.session_id
        assert table.get_row_at(table.cursor_row)[1] == "first question"
        status = app.screen.query_one("#session-status", Static)
        assert "Total cost: $0.000000" in str(status.render())


@pytest.mark.anyio
async def test_session_selector_deletes_highlighted_session_after_confirmation(
    tmp_path,
) -> None:
    store = SessionStore(tmp_path)
    first = store.create_session()
    second = store.create_session()
    store.update_session_title(first.session_id, "first question")
    store.update_session_title(second.session_id, "second question")
    app = AceAITUI(
        store.load_tui_events(first.session_id),
        session_recorder=SessionRecorder(store, first.session_id),
        session_id=first.session_id,
    )

    async with app.run_test() as pilot:
        app.open_session_selector()
        await pilot.pause(0.1)

        table = app.screen.query_one("#session-table", DataTable)
        second_row = _table_row_index(table, second.session_id)
        table.move_cursor(row=second_row)

        await pilot.press("d")
        await pilot.pause(0.1)

        message = app.screen.query_one("#delete-session-message", Static)
        assert second.session_id in str(message.content)

        await pilot.press("enter")
        await pilot.pause(0.1)

        table = app.screen.query_one("#session-table", DataTable)
        assert second.session_id not in [
            row.key.value for row in table.ordered_rows
        ]
        assert [session.session_id for session in store.list_sessions()] == [
            first.session_id
        ]


def _table_row_index(table: DataTable, session_id: str) -> int:
    for index, row in enumerate(table.ordered_rows):
        if row.key.value == session_id:
            return index
    raise ValueError(session_id)
