from __future__ import annotations

import os
import sys
import tempfile
import types
from pathlib import Path

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
os.environ.setdefault("PYNPUT_BACKEND", "dummy")

import numpy as np
import soundfile as sf

from app.core import audio_engine
from app.core.config import AppConfig, clear_saved_api_key
from app.core.ptt_controller import GlobalPTT
from app.core.updater import _version_tuple


def test_config_roundtrip() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        previous = os.environ.get("LOCALAPPDATA")
        os.environ["LOCALAPPDATA"] = tmp
        try:
            config = AppConfig(tts_voice="cedar", input_device_name="Mic", output_device_name="CABLE Input")
            config.save()
            loaded = AppConfig.load()
            assert loaded.tts_voice == "cedar"
            assert loaded.input_device_name == "Mic"
            assert loaded.output_device_name == "CABLE Input"
            assert not Path(tmp, "TiHiY Voice Translator", "settings.tmp").exists()
        finally:
            if previous is None:
                os.environ.pop("LOCALAPPDATA", None)
            else:
                os.environ["LOCALAPPDATA"] = previous



def test_api_key_is_private_and_resettable() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        previous = os.environ.get("LOCALAPPDATA")
        os.environ["LOCALAPPDATA"] = tmp
        try:
            config = AppConfig(api_key="private-test-value", tts_voice="cedar")
            config.save()
            assert AppConfig.load().api_key == "private-test-value"
            assert clear_saved_api_key() is True
            loaded = AppConfig.load()
            assert loaded.api_key == ""
            assert loaded.tts_voice == "cedar"
            assert clear_saved_api_key() is False
        finally:
            if previous is None:
                os.environ.pop("LOCALAPPDATA", None)
            else:
                os.environ["LOCALAPPDATA"] = previous


def test_legacy_migration_drops_api_key() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        previous = os.environ.get("LOCALAPPDATA")
        os.environ["LOCALAPPDATA"] = tmp
        try:
            legacy = Path(tmp, "TiHiY Voice Translator v9")
            legacy.mkdir(parents=True)
            legacy_payload = dict(api_key="private-legacy-value", tts_voice="cedar", ptt_key="F9")
            legacy.joinpath("settings.json").write_text(
                __import__("json").dumps(legacy_payload),
                encoding="utf-8",
            )
            loaded = AppConfig.load()
            assert loaded.api_key == ""
            assert loaded.tts_voice == "cedar"
            assert loaded.ptt_key == "F9"
        finally:
            if previous is None:
                os.environ.pop("LOCALAPPDATA", None)
            else:
                os.environ["LOCALAPPDATA"] = previous

def test_device_recovery() -> None:
    devices = [
        {"index": 2, "name": "Microphone (USB)", "inputs": 1, "outputs": 0},
        {"index": 7, "name": "CABLE Input (VB-Audio Virtual Cable)", "inputs": 0, "outputs": 2},
    ]
    assert audio_engine.find_device_by_name(devices, "microphone (usb)", direction="input") == 2
    assert audio_engine.choose_vb_cable_output(devices) == 7
    assert audio_engine.device_name(devices, 7).startswith("CABLE Input")


def test_ptt_press_is_idempotent() -> None:
    events: list[str] = []
    ptt = GlobalPTT("F8", lambda: events.append("down"), lambda: events.append("up"))
    ptt._press_once(); ptt._press_once(); ptt._release_once(); ptt._release_once()
    assert events == ["down", "up"]


class _FakeStream:
    def __init__(self, **kwargs):
        self.callback = kwargs["callback"]
        self.channels = kwargs["channels"]
    def start(self): pass
    def stop(self): pass
    def close(self): pass


class _FakeSD:
    def __init__(self):
        self.last_stream = None
        self.play_payload = None
    def query_devices(self, device=None, kind=None):
        if kind == "input":
            return {"default_samplerate": 48000, "max_input_channels": 1}
        if kind == "output":
            return {"default_samplerate": 48000, "max_output_channels": 2}
        return []
    def check_input_settings(self, **_kwargs): pass
    def check_output_settings(self, **_kwargs): pass
    def Stream(self, **kwargs):
        self.last_stream = _FakeStream(**kwargs)
        return self.last_stream
    def play(self, payload, samplerate, device, blocking):
        self.play_payload = (payload.copy(), samplerate, device, blocking)


def test_stereo_audio_path() -> None:
    fake = _FakeSD()
    original = audio_engine.sd
    audio_engine.sd = fake
    try:
        engine = audio_engine.DirectVoiceEngine()
        engine.start(1, 2, 48000, "Stereo")
        assert fake.last_stream.channels == (1, 2)
        indata = np.array([[0.25], [-0.5]], dtype=np.float32)
        outdata = np.zeros((2, 2), dtype=np.float32)
        fake.last_stream.callback(indata, outdata, 2, None, None)
        assert np.allclose(outdata[:, 0], indata[:, 0])
        assert np.allclose(outdata[:, 1], indata[:, 0])
        engine.stop()
        with tempfile.TemporaryDirectory() as tmp:
            wav = Path(tmp) / "voice.wav"
            sf.write(wav, indata, 48000)
            audio_engine.play_wav(wav, 2, "Stereo")
            payload = fake.play_payload[0]
            assert payload.shape[1] == 2
            assert np.allclose(payload[:, 0], payload[:, 1])
    finally:
        audio_engine.sd = original


def test_updater_version_comparison() -> None:
    assert _version_tuple("9.11.2") > _version_tuple("9.11.1")
    assert _version_tuple("v10.0") == (10, 0, 0)


def test_ui_without_runtime() -> None:
    from PySide6.QtWidgets import QApplication

    fake_openai = types.ModuleType("openai")
    fake_openai.OpenAI = object
    previous_openai = sys.modules.get("openai")
    sys.modules["openai"] = fake_openai
    import app.main as ui

    with tempfile.TemporaryDirectory() as tmp:
        previous = os.environ.get("LOCALAPPDATA")
        os.environ["LOCALAPPDATA"] = tmp
        app = QApplication.instance() or QApplication([])
        original_devices = ui.list_devices
        ui.list_devices = lambda: [
            {"index": 1, "name": "USB Mic", "hostapi": "Test", "inputs": 1, "outputs": 0, "default_samplerate": 48000},
            {"index": 2, "name": "CABLE Input (VB-Audio Virtual Cable)", "hostapi": "Test", "inputs": 0, "outputs": 2, "default_samplerate": 48000},
        ]
        try:
            window = ui.MainWindow(start_runtime=False)
            assert ui.VERSION == "9.11.2"
            assert window.said.toPlainText() == ""
            assert window.translated.toPlainText() == ""
            assert window.log_view.toPlainText() == ""
            assert window.config.input_device == 1
            assert window.config.output_device == 2
            assert window.settings.voice_combo.count() >= 13

            # A second process must not become another running copy.
            guard_name = f"TiHiY_self_test_{os.getpid()}_{id(window)}"
            primary_guard = ui.SingleInstanceGuard(guard_name, app)
            secondary_guard = ui.SingleInstanceGuard(guard_name, app)
            assert primary_guard.acquire() is True
            assert secondary_guard.acquire() is False
            secondary_guard.close(); primary_guard.close()

            window.stop_runtime(); window.settings.close(); window.close(); app.processEvents()
        finally:
            ui.list_devices = original_devices
            if previous_openai is None:
                sys.modules.pop("openai", None)
            else:
                sys.modules["openai"] = previous_openai
            if previous is None:
                os.environ.pop("LOCALAPPDATA", None)
            else:
                os.environ["LOCALAPPDATA"] = previous


def main() -> int:
    tests = [
        test_config_roundtrip,
        test_api_key_is_private_and_resettable,
        test_legacy_migration_drops_api_key,
        test_device_recovery,
        test_ptt_press_is_idempotent,
        test_stereo_audio_path,
        test_updater_version_comparison,
        test_ui_without_runtime,
    ]
    for test in tests:
        test()
        print(f"PASS: {test.__name__}")
    print(f"SELF-TEST OK: {len(tests)} tests")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
