from __future__ import annotations

import asyncio
import logging
import struct
import wave
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace

from app.utils import full_call_recording
from app.utils.full_call_recording import (
    EntireCallRecorder,
    EntireCallUploadItem,
    build_entire_call_object_name,
    enqueue_entire_call_upload,
)
from app.utils.stt_audio_upload import OCIUploadConfig


def test_entire_call_object_name_uses_date_folder_and_session_filename() -> None:
    object_name = build_entire_call_object_name(
        session_id="sessao muito longa/" * 30,
        now=datetime(2026, 7, 8, 10, 30, 0),
    )

    parts = object_name.split("/")
    assert parts[0] == "2026-07-08"
    assert parts[1] == "entire_call"
    assert object_name.endswith(".wav")
    assert len(parts) == 3
    assert len(parts[2]) <= 68
    assert " " not in object_name


def test_entire_call_recorder_writes_stereo_wav_without_upload(tmp_path: Path) -> None:
    async def _run() -> Path:
        recorder = EntireCallRecorder(
            session_id="session-1",
            sample_rate=1000,
            channels=1,
            sample_width=2,
            frame_ms=20,
            bytes_per_frame=4,
            tmp_dir=tmp_path,
            logger_override=logging.getLogger("test.full_call_recording.wav"),
            object_name="2026-07-08/entire_call/session-1.wav",
        )
        recorder.start()
        recorder.record_client_frame(struct.pack("<hh", 1000, -1000))
        recorder.record_output_frame(struct.pack("<hh", 2000, 500))
        result = await recorder.finalize(enqueue_upload=False)

        assert result is not None
        assert result.frames == 1
        assert result.duration_ms == 20
        assert result.upload_enqueued is False
        return result.path

    wav_path = asyncio.run(_run())
    with wave.open(str(wav_path), "rb") as handle:
        assert handle.getframerate() == 1000
        assert handle.getnchannels() == 2
        assert handle.getsampwidth() == 2
        # Stereo samples are interleaved as client-left, agent-right.
        assert struct.unpack("<hhhh", handle.readframes(2)) == (
            1000,
            2000,
            -1000,
            500,
        )


def test_entire_call_recorder_keeps_each_side_in_its_own_channel(tmp_path: Path) -> None:
    async def _run() -> Path:
        recorder = EntireCallRecorder(
            session_id="session-separated",
            sample_rate=1000,
            channels=1,
            sample_width=2,
            frame_ms=20,
            bytes_per_frame=4,
            tmp_dir=tmp_path,
            logger_override=logging.getLogger("test.full_call_recording.separated"),
        )
        recorder.start()
        recorder.record_output_frame(struct.pack("<hh", 300, 400))
        recorder.record_client_frame(struct.pack("<hh", 100, 200))
        result = await recorder.finalize(enqueue_upload=False)
        assert result is not None
        return result.path

    with wave.open(str(asyncio.run(_run())), "rb") as handle:
        assert handle.getnchannels() == 2
        samples = struct.unpack("<hhhhhhhh", handle.readframes(4))
        assert samples == (
            0,
            300,
            0,
            400,
            100,
            0,
            200,
            0,
        )


def test_entire_call_recorder_accepts_metadata_with_reserved_names(tmp_path: Path) -> None:
    async def _run() -> None:
        recorder = EntireCallRecorder(
            session_id="session-1",
            sample_rate=1000,
            channels=1,
            sample_width=2,
            frame_ms=20,
            bytes_per_frame=4,
            tmp_dir=tmp_path,
            logger_override=logging.getLogger("test.full_call_recording.metadata"),
            metadata={"session_id": "session-1", "room": "room-1"},
            object_name="2026-07-08/entire_call/session-1.wav",
        )

        recorder.start()
        recorder.record_output_frame(b"\x00\x00\x00\x00")
        result = await recorder.finalize(enqueue_upload=False)

        assert result is not None

    asyncio.run(_run())


def test_entire_call_recorder_queue_full_drops_without_raising(tmp_path: Path) -> None:
    recorder = EntireCallRecorder(
        session_id="session-1",
        sample_rate=1000,
        channels=1,
        sample_width=2,
        frame_ms=20,
        bytes_per_frame=4,
        tmp_dir=tmp_path,
        logger_override=logging.getLogger("test.full_call_recording.drop"),
        queue_size=1,
    )
    recorder._started = True
    recorder._queue.put_nowait(object())

    recorder.record_client_frame(b"\x00\x00\x00\x00")

    assert recorder.dropped_queue_frames == 1


def test_upload_item_streams_file_to_oci(monkeypatch, tmp_path: Path) -> None:
    calls: dict[str, object] = {}
    wav_path = tmp_path / "call.wav"
    wav_path.write_bytes(b"fake-wav")
    config = OCIUploadConfig(
        auth_mode="oke_workload_identity",
        region="sa-saopaulo-1",
        bucket="tia-audio",
        namespace="namespace",
    )

    class FakeClient:
        def put_object(self, **kwargs):
            calls["namespace_name"] = kwargs["namespace_name"]
            calls["bucket_name"] = kwargs["bucket_name"]
            calls["object_name"] = kwargs["object_name"]
            calls["body"] = kwargs["put_object_body"].read()
            return SimpleNamespace(status=200)

    monkeypatch.setattr(full_call_recording, "_oci_upload_config_from_env", lambda: config)
    monkeypatch.setattr(full_call_recording, "_oci_client", lambda _config: FakeClient())

    item = EntireCallUploadItem(
        object_name="2026-07-08/entire_call/session-1.wav",
        path=wav_path,
        session_id="session-1",
        bytes=8,
        duration_ms=20,
        dropped_queue_frames=0,
        dropped_client_buffer_frames=0,
        metadata={},
    )

    asyncio.run(full_call_recording._upload_item(item))

    assert calls == {
        "namespace_name": "namespace",
        "bucket_name": "tia-audio",
        "object_name": "2026-07-08/entire_call/session-1.wav",
        "body": b"fake-wav",
    }


def test_upload_item_retries_with_a_fresh_client_after_transient_failure(
    monkeypatch,
    tmp_path: Path,
) -> None:
    wav_path = tmp_path / "call.wav"
    wav_path.write_bytes(b"fake-wav")
    config = OCIUploadConfig(
        auth_mode="oke_workload_identity",
        region="sa-saopaulo-1",
        bucket="tia-audio",
        namespace="namespace",
    )
    attempts = 0
    resets = 0

    class FakeClient:
        def put_object(self, **_kwargs):
            nonlocal attempts
            attempts += 1
            if attempts == 1:
                raise RuntimeError("transient connection failure")
            return SimpleNamespace(status=200)

    def fake_reset() -> None:
        nonlocal resets
        resets += 1

    monkeypatch.setenv("ENTIRE_CALL_RECORDING_UPLOAD_RETRY_BASE_DELAY_S", "0")
    monkeypatch.setattr(full_call_recording, "_oci_upload_config_from_env", lambda: config)
    monkeypatch.setattr(full_call_recording, "_oci_client", lambda _config: FakeClient())
    monkeypatch.setattr(full_call_recording, "_reset_oci_client", fake_reset)

    item = EntireCallUploadItem(
        object_name="2026-07-08/entire_call/session-retry.wav",
        path=wav_path,
        session_id="session-retry",
        bytes=8,
        duration_ms=20,
        dropped_queue_frames=0,
        dropped_client_buffer_frames=0,
        metadata={},
    )

    asyncio.run(full_call_recording._upload_item(item))

    assert attempts == 2
    assert resets == 1


def test_upload_worker_keeps_file_after_all_attempts_fail(monkeypatch, tmp_path: Path) -> None:
    async def _run() -> None:
        wav_path = tmp_path / "failed.wav"
        wav_path.write_bytes(b"recoverable-wav")
        item = EntireCallUploadItem(
            object_name="2026-07-08/entire_call/session-failed.wav",
            path=wav_path,
            session_id="session-failed",
            bytes=15,
            duration_ms=20,
            dropped_queue_frames=0,
            dropped_client_buffer_frames=0,
            metadata={},
        )

        async def fail_upload(_item) -> None:
            raise RuntimeError("OCI unavailable")

        monkeypatch.setattr(full_call_recording, "_upload_item", fail_upload)
        worker = full_call_recording._EntireCallUploadWorker(asyncio.get_running_loop())
        assert worker.enqueue(item)
        await asyncio.wait_for(worker._queue.join(), timeout=1)

        assert wav_path.read_bytes() == b"recoverable-wav"
        worker._task.cancel()

    asyncio.run(_run())


def test_enqueue_entire_call_upload_is_noop_and_keeps_file_without_oci_config(
    monkeypatch,
    tmp_path: Path,
) -> None:
    wav_path = tmp_path / "call.wav"
    wav_path.write_bytes(b"fake-wav")
    monkeypatch.setattr(full_call_recording, "_oci_upload_config_from_env", lambda: None)

    assert (
        enqueue_entire_call_upload(
            path=wav_path,
            object_name="2026-07-08/entire_call/session-1.wav",
            session_id="session-1",
            bytes=8,
            duration_ms=20,
            logger_override=logging.getLogger("test.full_call_recording.noop"),
        )
        is None
    )
    assert wav_path.read_bytes() == b"fake-wav"
