"""Test the basic file editing functionality."""

from __future__ import annotations

import contextlib
import dataclasses
import functools
import os
import pathlib
import shlex
import tempfile
import time
import typing

import libtmux
import pytest


if typing.TYPE_CHECKING:
    from collections.abc import Iterator
    from typing import Final


CONTENTS_TESTFILE: Final = "This is a test.\nThis is only a test.\n"
"""The contents of the testfile to create and edit."""

LINES_TESTFILE_START_FIRST: Final = [
    " 00000000:  54 68 69 73 20 69 73 20  61 20 74 65 73 74 2e 0a  This is a test..",
    " 00000010:  54 68 69 73 20 69 73 20  6f 6e 6c 79 20 61 20 74  This is only a t",
    " 00000020:  65 73 74 2e 0a -- -- --  -- -- -- -- -- -- -- --  est..-----------",
    "~",
    "~",
]
"""The first lines of the initial contents of the screen once the testfile has been opened."""


@dataclasses.dataclass(frozen=True)
class Tmux:
    """A `tmux` server running in a temporary directory."""

    srv: libtmux.Server
    """The `tmux` server object."""

    cmd: str
    """The command we started within the server."""

    pane: libtmux.Pane
    """The pane that the program was started in."""


@functools.lru_cache
def hexer_prog() -> str:
    """Get the `hexer` program to use."""
    return os.environ.get("HEXER_PROG", "hexer")


@contextlib.contextmanager
def tempdir() -> Iterator[pathlib.Path]:
    """Create a temporary directory, change into it, change back out."""
    with tempfile.TemporaryDirectory() as tempd_obj:
        tempd: Final = pathlib.Path(tempd_obj)
        print(f"Using {tempd} as a temporary directory")
        yield tempd


@contextlib.contextmanager
def start_tmux_in_tempdir(tempd: pathlib.Path, cmd: str) -> Iterator[Tmux]:
    """Create a temporary directory, start a `tmux` server within it."""
    socket_path: Final = tempd / "tmux-test.sock"
    print(f"Preparing a tmux server at {socket_path}")
    srv: Final = libtmux.Server(socket_path=socket_path)
    try:
        assert not srv.is_alive()
        assert not srv.sessions

        print(f"Starting a `{cmd}` session at {srv.socket_path}")
        sess: Final = srv.new_session(window_command=cmd)
        time.sleep(0.1)
        assert srv.is_alive()

        match srv.sessions:
            case [single] if single == sess:
                match sess.windows:
                    case [win]:
                        match win.panes:
                            case [pane]:
                                yield Tmux(srv=srv, cmd=cmd, pane=pane)

                            case other_panes:
                                pytest.fail(repr(other_panes))

                    case other_windows:
                        pytest.fail(repr(other_windows))

            case other_sessions:
                pytest.fail(repr(other_sessions))
    finally:
        if srv.is_alive():
            print(f"Stopping the tmux server at {socket_path}")
            if hasattr(srv, "kill"):
                srv.kill()
            else:
                srv.kill_server()
        else:
            print(f"The tmux server at {socket_path} seems to have gone away already")


@contextlib.contextmanager
def hexer_testfile() -> Iterator[tuple[pathlib.Path, Tmux]]:
    """Create the test file, start `hexer`, wait for it to load the file."""
    with tempdir() as tempd:
        path: Final = tempd / "file.txt"
        path.write_text(CONTENTS_TESTFILE, encoding="UTF-8")

        with start_tmux_in_tempdir(tempd, shlex.join([hexer_prog(), "--", str(path)])) as tmux:
            assert tmux.srv.is_alive()

            print("Waiting for hexer to load the file")
            for _ in range(20):
                lines = tmux.pane.capture_pane()
                if lines and lines[0].startswith(" 0000") and not lines[-1].endswith(" ..."):
                    print("Loaded")
                    break
                time.sleep(0.1)
            else:
                pytest.fail(repr(lines))

            assert lines[: len(LINES_TESTFILE_START_FIRST)] == LINES_TESTFILE_START_FIRST
            assert lines[-1].startswith(f' "{path}" ')

            yield path, tmux


def tmux_check_text_at(pane: libtmux.Pane, x: int, y: int, needle: str) -> None:
    """Make sure the expected text is at the expected position on the screen right now."""
    print(f"Looking for {needle!r} at ({x + 1}, {y + 1})")
    lines: Final = pane.capture_pane()
    assert len(lines) > y
    line: Final = lines[y]
    assert len(line) > x
    assert line[x:].startswith(needle)


def hexer_quit(tmux: Tmux, *, command: str = ":q") -> None:
    """Send ':q' to hexer, wait for it to go away."""
    print(f"Sending {command!r} to the editor")
    tmux.pane.send_keys(command, enter=True, literal=True)

    print("Waiting for the editor to go away")
    for _ in range(20):
        if not tmux.srv.is_alive():
            print("Gone away")
            break
        time.sleep(0.1)
    else:
        pytest.fail(repr(tmux.srv))


def hexer_wait_for_status(tmux: Tmux, needle: str) -> None:
    """Wait for the status line to show a specific message."""
    print(f"Waiting for {needle!r} in the hexer status line")
    for _ in range(20):
        assert tmux.srv.is_alive()
        lines = tmux.pane.capture_pane()
        assert lines
        if needle in lines[-1]:
            print(f"Got {lines[-1]!r}")
            break
        time.sleep(0.1)
    else:
        pytest.fail(repr(tmux.srv))


def hexer_check_position(tmux: Tmux, pos: int) -> None:
    """Send Ctrl-G, wait for the position in the status line."""
    print("Sending Ctrl-G to hexer")
    tmux.pane.send_keys("\x07", literal=True)
    hexer_wait_for_status(tmux, f" at 0x{pos:x} ({pos})")


def test_start_quit() -> None:
    """Start `hexer` on an existing file, check the screen, quit."""
    print()

    with hexer_testfile() as (_path, tmux):
        hexer_quit(tmux)


def test_start_search_replace_quit() -> None:
    """Look for a string, replace the first byte."""
    print()

    with hexer_testfile() as (path, tmux):
        hexer_check_position(tmux, 0)
        tmux_check_text_at(tmux.pane, 70, 1, "only")

        tmux.pane.send_keys("/on", enter=True, literal=True)
        hexer_check_position(tmux, 24)

        tmux.pane.send_keys("r", enter=False, literal=True)
        hexer_wait_for_status(tmux, "replace byte at position ")

        tmux.pane.send_keys("4f", enter=False, literal=True)
        hexer_check_position(tmux, 24)
        tmux_check_text_at(tmux.pane, 70, 1, "Only")

        tmux.pane.send_keys(":q", enter=True, literal=True)
        hexer_wait_for_status(tmux, "no write since last change")

        hexer_quit(tmux, command=":wq")

        print(f"Making sure {path} was modified")
        assert path.read_text(encoding="UTF-8") == CONTENTS_TESTFILE.replace("only", "Only")
