"""
Тесты нативного ZIP: unzip → buildZip.

Эмулирует JS-логику:
1. Сервер возвращает ZIP с .md + mapping.csv
2. unzip() распаковывает
3. buildZip() перепаковывает без mapping.csv
4. Результат должен быть валидным ZIP

Запуск: python3 tests/test_zip.py
"""

import io
import zipfile
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

passed = 0
failed = 0


def check(name, condition):
    global passed, failed
    if condition:
        passed += 1
        print(f"  ✅ {name}")
    else:
        failed += 1
        print(f"  ❌ {name}")


# ═══════════════════════════════════════════════════════════════
# Test 1: roundtrip — server ZIP → extract → rebuild → verify
# ═══════════════════════════════════════════════════════════════
print("\n=== Тест 1: unzip + buildZip roundtrip ===")

# Создаём ZIP как сервер
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    zf.writestr("договор_obfuscated.md", "# Заголовок\nТекст договора с Иванов_0001.")
    zf.writestr("спецификация_obfuscated.md", "| Услуга | Цена |\n|---|---|\n| Хостинг | 1000 |")
    zf.writestr("mapping.csv", "тип,оригинал,замена\nperson,Иванов И.И.,Иванов_0001\n")

server_zip = buf.getvalue()

# Распаковываем (эмуляция unzip)
with zipfile.ZipFile(io.BytesIO(server_zip)) as zf:
    extracted = {n: zf.read(n) for n in zf.namelist()}

check("3 файла в ZIP", len(extracted) == 3)
check("mapping.csv есть", "mapping.csv" in extracted)
check("договор есть", "договор_obfuscated.md" in extracted)

# Отделяем mapping от файлов
md_files = {n: d for n, d in extracted.items() if n != "mapping.csv"}
mapping_data = extracted.get("mapping.csv", b"").decode("utf-8")

check("mapping читается", "Иванов_0001" in mapping_data)
check("договор читается", "Иванов_0001" in md_files["договор_obfuscated.md"].decode("utf-8"))

# Перепаковываем без mapping.csv (эмуляция buildZip)
buf2 = io.BytesIO()
with zipfile.ZipFile(buf2, 'w') as zf:
    for name, data in md_files.items():
        zf.writestr(name, data)
rebuilt_zip = buf2.getvalue()

# Проверяем перепакованный ZIP
with zipfile.ZipFile(io.BytesIO(rebuilt_zip)) as zf:
    names = zf.namelist()
    check("mapping.csv НЕТ в перепакованном", "mapping.csv" not in names)
    check("договор есть в перепакованном", "договор_obfuscated.md" in names)
    check("спецификация есть в перепакованном", "спецификация_obfuscated.md" in names)
    content = zf.read("договор_obfuscated.md").decode("utf-8")
    check("содержимое договора сохранено", "Иванов_0001" in content)


# ═══════════════════════════════════════════════════════════════
# Test 2: ZIP с одним файлом
# ═══════════════════════════════════════════════════════════════
print("\n=== Тест 2: один файл без mapping ===")

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    zf.writestr("file.md", "content")

server_zip = buf.getvalue()

with zipfile.ZipFile(io.BytesIO(server_zip)) as zf:
    extracted = {n: zf.read(n) for n in zf.namelist()}

md_files = {n: d for n, d in extracted.items() if n != "mapping.csv"}

buf2 = io.BytesIO()
with zipfile.ZipFile(buf2, 'w') as zf:
    for name, data in md_files.items():
        zf.writestr(name, data)

with zipfile.ZipFile(io.BytesIO(buf2.getvalue())) as zf:
    check("один файл", len(zf.namelist()) == 1)
    check("контент сохранён", zf.read("file.md").decode("utf-8") == "content")


# ═══════════════════════════════════════════════════════════════
# Test 3: .doc файл (binary, не меняется)
# ═══════════════════════════════════════════════════════════════
print("\n=== Тест 3: бинарный .doc ===")

binary_data = bytes([0xD0, 0xCF, 0x11, 0xE0]) + b"\x00" * 100  # OLE2 magic

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    zf.writestr("file_obfuscated.doc", binary_data)
    zf.writestr("mapping.csv", "тип,оригинал,замена\n")

with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf:
    extracted = {n: zf.read(n) for n in zf.namelist()}

md_files = {n: d for n, d in extracted.items() if n != "mapping.csv"}

buf2 = io.BytesIO()
with zipfile.ZipFile(buf2, 'w') as zf:
    for name, data in md_files.items():
        zf.writestr(name, data)

with zipfile.ZipFile(io.BytesIO(buf2.getvalue())) as zf:
    check(".doc сохранён", "file_obfuscated.doc" in zf.namelist())
    content = zf.read("file_obfuscated.doc")
    check("размер сохранён", len(content) == len(binary_data))
    check("данные сохранены", content == binary_data)


# ═══════════════════════════════════════════════════════════════
# Test 4: CSV с кириллицей и кавычками
# ═══════════════════════════════════════════════════════════════
print("\n=== Тест 4: CSV с кириллицей ===")

csv_content = 'тип,оригинал,замена\ncompany,"ООО ""НУБЕС""",ООО_Технология_0001\nperson,Иванов И.И.,Иванов_0002\n'

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    zf.writestr("test.md", "text")
    zf.writestr("mapping.csv", csv_content)

with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf:
    mapping = zf.read("mapping.csv").decode("utf-8")

check("BOM есть (\\ufeff)", mapping.startswith('\ufeff') or mapping.startswith('тип'))
check("НУБЕС в mapping", "НУБЕС" in mapping)
check("Иванов в mapping", "Иванов" in mapping)


# ═══════════════════════════════════════════════════════════════
# Test 5: множественные mapping.csv объединяются
# ═══════════════════════════════════════════════════════════════
print("\n=== Тест 5: объединение mapping.csv ===")

all_mappings = []
for csv_text in [
    "тип,оригинал,замена\nperson,Иванов,Иванов_0001\n",
    "тип,оригинал,замена\nperson,Петров,Петров_0002\ncompany,Ромашка,Ромашка_0001\n",
]:
    lines = csv_text.split("\n")
    if len(all_mappings) == 0:
        all_mappings.append(lines[0])  # заголовок
        all_mappings.append(lines[1])  # первая строка
    else:
        all_mappings.append(lines[1])  # только данные, без заголовка
        if len(lines) > 2 and lines[2]:
            all_mappings.append(lines[2])

result = "\n".join(all_mappings)
check("заголовок один", result.count("тип,оригинал,замена") == 1)
check("Иванов есть", "Иванов" in result)
check("Петров есть", "Петров" in result)
check("Ромашка есть", "Ромашка" in result)


# ═══════════════════════════════════════════════════════════════
# Test 6: точные смещения в local file header (баг start+18)
# ═══════════════════════════════════════════════════════════════
print("\n=== Тест 6: валидация смещений local file header ===")

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    zf.writestr("test.md", "Hello World")
raw = buf.getvalue()

import struct
sig_pos = raw.find(b'PK\x03\x04')
check("сигнатура найдена", sig_pos >= 0)

def u16(off): return struct.unpack_from('<H', raw, off)[0]
def u32(off): return struct.unpack_from('<I', raw, off)[0]

start = sig_pos
check("signature = 0x04034b50", u32(start) == 0x04034b50)

name_len = u16(start + 26)
extra_len = u16(start + 28)
comp_size = u32(start + 18)   # смещение которое было сломано
uncomp_size = u32(start + 22)

check("compressed size > 0", comp_size > 0)
check("compressed == uncompressed", comp_size == uncomp_size)
check("filename length", name_len == len("test.md"))

name_start = start + 30
name = raw[name_start:name_start + name_len].decode('ascii')
check("имя файла", name == "test.md")

data_start = name_start + name_len + extra_len
data = raw[data_start:data_start + comp_size].decode('ascii')
check("данные файла", data == "Hello World")


# ═══════════════════════════════════════════════════════════════
# Итог
# ═══════════════════════════════════════════════════════════════
print(f"\n{'='*50}")
print(f"Результат: {passed} OK, {failed} FAIL из {passed + failed}")
if failed > 0:
    print("❌ ТЕСТЫ ПРОВАЛЕНЫ")
    sys.exit(1)
else:
    print("✅ ВСЕ ТЕСТЫ ПРОЙДЕНЫ")
