'''
Storage command UX regressions.

'''
from collections.abc import (
    AsyncIterator,
    Iterator,
)
from contextlib import asynccontextmanager
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any

import numpy as np
import pytest
from typer.testing import CliRunner

from piker import tsp
from piker import config
from piker.data import def_iohlcv_fields
from piker.storage import cli as storage_cli
from piker.storage.cli import store


def test_store_group_shows_help_without_arguments() -> None:
    '''
    Bare ``piker store`` must present its command map immediately.

    The Typer group emitted only a missing-command error and
    required another ``--help`` invocation. Invoke the group
    without arguments and prove complete help replaces that error.

    '''
    result = CliRunner().invoke(store, [])

    assert result.exit_code == 2
    assert 'Usage:' in result.output
    assert 'Missing command' not in result.output
    for command in (
        'anal',
        'audit',
        'delete',
        'ls',
        'series',
        'shm',
    ):
        assert command in result.output


def test_store_commands_show_help_without_arguments() -> None:
    '''
    Bare endpoints must not open runtimes or report missing args.

    Every endpoint is discoverable by typing its name once. Exercise
    required-input commands and prove help rendering exits before any
    callback can run.

    '''
    runner = CliRunner()
    for command in (
        'anal',
        'audit',
        'delete',
        'shm',
    ):
        result = runner.invoke(store, [command])
        assert result.exit_code == 2
        assert 'Usage:' in result.output
        assert 'Missing argument' not in result.output


def test_series_lists_exact_native_periods(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    '''
    Durable listing must expose separate 1s and 60s identities.

    The old FQME-only index hid one timeframe. Create canonical files
    without storage or actor services, invoke the JSON endpoint,
    and prove both exact paths and periods are returned.

    '''
    nativedb: Path = tmp_path / 'nativedb'
    nativedb.mkdir()
    one = nativedb / 'qqq.nasdaq.ib.ohlcv1s.parquet'
    sixty = nativedb / 'qqq.nasdaq.ib.ohlcv60s.parquet'
    three_hundred = nativedb / (
        'qqq.nasdaq.ib.ohlcv300s.parquet'
    )
    one.write_bytes(b'1')
    sixty.write_bytes(b'60')
    three_hundred.write_bytes(b'300')
    monkeypatch.setattr(config, 'get_conf_dir', lambda: tmp_path)

    result = CliRunner().invoke(
        store,
        ['series', '--json'],
    )

    assert result.exit_code == 0
    payload = json.loads(result.output)
    assert [item['period_s'] for item in payload] == [1, 60, 300]
    assert [item['path'] for item in payload] == [
        str(one),
        str(sixty),
        str(three_hundred),
    ]

    compact = CliRunner().invoke(store, ['ls'])
    assert compact.exit_code == 0
    assert 'qqq.nasdaq.ib' in compact.output
    assert '1s, 60s, 300s' in compact.output


def test_shm_reload_requires_explicit_persistence() -> None:
    '''
    SHM reload must not silently opt into durable persistence.

    Reload reads back the Parquet produced by the repair path, so the
    reload flag alone has no valid input and must not open runtime or
    storage services. Invoke that invalid combination and prove CLI
    validation rejects it before inspecting an FQME.

    '''
    result = CliRunner().invoke(
        store,
        ['shm', 'qqq.nasdaq.ib', '--reload-parquet-to-shm'],
    )

    assert result.exit_code == 2
    assert 'requires --write-parquet' in result.output


@pytest.mark.parametrize(
    'option',
    [
        ['--json'],
        ['--max-gaps', '10'],
    ],
)
def test_shm_repair_rejects_read_only_formatting(
    option: list[str],
) -> None:
    '''
    Repair mode must not silently ignore read-only output options.

    Combining the old repair workflow with the new inspector made
    ``--json`` and an explicitly default-valued ``--max-gaps``
    appeared accepted even though repair produced logs and entered an
    interactive pause. Invoke each conflicting option and prove
    validation stops before runtime or storage can be opened.

    '''
    result = CliRunner().invoke(
        store,
        [
            'shm',
            'qqq.nasdaq.ib',
            '--write-parquet',
            *option,
        ],
    )

    assert result.exit_code == 2
    assert 'apply only to read-only output' in result.output


def test_ls_has_description_in_store_help() -> None:
    '''
    Compact NativeDB discovery must explain itself in group help.

    The backend-oriented ``ls`` callback had no docstring, leaving
    its command-map description blank. Render the parent help and
    prove the compact series purpose is visible without opening the
    command.

    '''
    result = CliRunner().invoke(store, [])

    assert result.exit_code == 2
    assert 'List NativeDB FQMEs' in result.output


def test_shm_endpoint_reports_immutable_snapshot(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    '''
    SHM diagnostics report exact buffers without storage mutation.

    Build one RT identity and snapshot, then replace runtime
    attachment with fakes. Invoke JSON output and prove it carries
    generation, cadence, and invalid-slot
    evidence without opening a storage client or writer.

    '''
    path = Path(
        'piker.datad[aaaaaaaa-aaaa-aa].qqq.nasdaq.ib.rt'
    )
    ref = tsp.ShmBufferRef(
        service='datad',
        generation='aaaaaaaa-aaaa-aa',
        fqme='qqq.nasdaq.ib',
        kind='rt',
        path=path,
    )
    frame = np.zeros(
        4,
        dtype=np.dtype(def_iohlcv_fields),
    )
    frame['index'] = np.arange(4)
    frame['time'] = [1, 0, 3, 4]

    @asynccontextmanager
    async def open_runtime(
        *args: Any,
        **kwargs: Any,
    ) -> AsyncIterator[None]:
        '''
        Yield a runtime-free diagnostic context.

        '''
        yield

    def iter_refs(
        fqme: str|None = None,
    ) -> Iterator[tsp.ShmBufferRef]:
        '''
        Yield the selected exact SHM identity.

        '''
        yield ref

    def iter_frames(
        fqme: str,
        shm_name: str|None = None,
        refs: list[tsp.ShmBufferRef]|None = None,
    ) -> Iterator[tuple[Path, SimpleNamespace, None]]:
        '''
        Yield an immutable snapshot through the attachment interface.

        '''
        shm = SimpleNamespace(array=frame)
        yield path, shm, None

    def fail_storage(*args: Any, **kwargs: Any) -> None:
        '''
        Fail if read-only SHM inspection reaches storage.

        '''
        raise AssertionError('SHM diagnostics opened storage')

    monkeypatch.setattr(
        storage_cli,
        'open_piker_runtime',
        open_runtime,
    )
    monkeypatch.setattr(
        storage_cli,
        'open_storage_client',
        fail_storage,
    )
    monkeypatch.setattr(tsp, 'iter_shm_buffer_refs', iter_refs)
    monkeypatch.setattr(tsp, 'iter_dfs_from_shms', iter_frames)

    result = CliRunner().invoke(
        store,
        ['shm', 'qqq.nasdaq.ib', '--json'],
    )

    assert result.exit_code == 0
    payload = json.loads(result.output)
    assert len(payload) == 1
    assert payload[0]['generation'] == 'aaaaaaaa-aaaa-aa'
    assert payload[0]['period_s'] == 1
    assert payload[0]['invalid_count'] == 1


def test_shm_rejects_unknown_exact_name_before_runtime(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    '''
    A stale exact SHM selection must fail before opening services.

    The old no-match path paused after opening runtime and storage
    setup. Return no identity, install a runtime fail-spy,
    and prove exact selection exits immediately
    with a deterministic error.

    '''
    def iter_refs(
        fqme: str|None = None,
    ) -> Iterator[tsp.ShmBufferRef]:
        '''
        Yield no matching SHM identities.

        '''
        return iter(())

    def fail_runtime(*args: Any, **kwargs: Any) -> None:
        '''
        Fail if no-match handling opens a runtime.

        '''
        raise AssertionError('unknown SHM opened runtime')

    monkeypatch.setattr(tsp, 'iter_shm_buffer_refs', iter_refs)
    monkeypatch.setattr(
        storage_cli,
        'open_piker_runtime',
        fail_runtime,
    )

    result = CliRunner().invoke(
        store,
        [
            'shm',
            'qqq.nasdaq.ib',
            '--shm-name',
            'missing',
        ],
    )

    assert result.exit_code == 2
    assert 'No exact OHLCV SHM buffer' in result.output
