"""Tests for TerminalService and terminal WebSocket endpoint."""

import json
import struct
from unittest.mock import patch

import pytest

from hangar.services.terminal import TerminalService, TerminalSession


class TestTerminalSession:
    """Unit tests for TerminalSession."""

    def test_write_after_close_is_noop(self):
        session = TerminalSession(master_fd=-1, pid=-1)
        session._closed = True
        session.write(b"hello")

    def test_resize_after_close_is_noop(self):
        session = TerminalSession(master_fd=-1, pid=-1)
        session._closed = True
        session.resize(100, 50)

    def test_closed_property(self):
        session = TerminalSession(master_fd=-1, pid=-1)
        assert not session.closed
        session._closed = True
        assert session.closed

    @pytest.mark.asyncio
    async def test_read_when_closed_returns_none(self):
        session = TerminalSession(master_fd=-1, pid=-1)
        session._closed = True
        result = await session.read()
        assert result is None

    def test_close_handles_missing_process(self):
        session = TerminalSession(master_fd=-1, pid=999999)
        session.close()
        assert session.closed

    def test_write_sends_to_fd(self):
        session = TerminalSession(master_fd=42, pid=1)
        with patch("os.write") as mock_write:
            session.write(b"hello")
            mock_write.assert_called_once_with(42, b"hello")

    def test_write_marks_closed_on_oserror(self):
        session = TerminalSession(master_fd=42, pid=1)
        with patch("os.write", side_effect=OSError("broken pipe")):
            session.write(b"hello")
        assert session.closed

    def test_resize_calls_ioctl(self):
        session = TerminalSession(master_fd=42, pid=1)
        with patch("hangar.services.terminal.fcntl.ioctl") as mock_ioctl:
            session.resize(120, 40)
            mock_ioctl.assert_called_once()
            args = mock_ioctl.call_args
            assert args[0][0] == 42
            winsize = struct.unpack("HHHH", args[0][2])
            assert winsize[0] == 40  # rows
            assert winsize[1] == 120  # cols

    def test_close_sends_sigterm_and_closes_fd(self):
        session = TerminalSession(master_fd=42, pid=12345)
        with (
            patch("os.killpg") as mock_killpg,
            patch("os.getpgid", return_value=12345),
            patch("os.close") as mock_close,
            patch("os.waitpid"),
        ):
            session.close()
            mock_killpg.assert_called_once()
            mock_close.assert_called_once_with(42)
            assert session.closed

    def test_close_idempotent(self):
        session = TerminalSession(master_fd=-1, pid=-1)
        session.close()
        session.close()  # Second call should be noop
        assert session.closed


class TestTerminalService:
    """Unit tests for TerminalService."""

    def test_initial_state(self):
        service = TerminalService()
        assert not service.has_active_session
        assert service.session is None

    def test_close_session_when_no_session(self):
        service = TerminalService()
        service.close_session()

    def test_create_session_closes_existing(self):
        service = TerminalService()
        mock_session = TerminalSession(master_fd=-1, pid=-1)
        service._session = mock_session

        with patch.object(service, 'create_session') as mock_create:
            # Verify the old session would be closed
            assert not mock_session.closed

    def test_has_active_session_false_when_closed(self):
        service = TerminalService()
        session = TerminalSession(master_fd=-1, pid=-1)
        session._closed = True
        service._session = session
        assert not service.has_active_session
