import hashlib
import hmac
import tempfile
from pathlib import Path

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

import wechat_decrypt_tool.wechat_decrypt as wechat_decrypt
from wechat_decrypt_tool.wechat_decrypt import (
    HMAC_SIZE,
    PAGE_SIZE,
    RESERVE_SIZE,
    SALT_SIZE,
    SQLITE_HEADER,
    WeChatDatabaseDecryptor,
    _derive_mac_key,
    _derive_sqlcipher_enc_key,
)


def _build_plain_page(fill: int, *, first_page: bool) -> bytes:
    body = bytes([fill]) * (PAGE_SIZE - RESERVE_SIZE)
    if first_page:
        body = SQLITE_HEADER + body[len(SQLITE_HEADER):]
    return body + (b"\x00" * RESERVE_SIZE)


def _encrypt_page(key_material: bytes, plain_page: bytes, page_num: int, salt: bytes, iv: bytes, *, passphrase: bool = False) -> bytes:
    enc_key = _derive_sqlcipher_enc_key(key_material, salt) if passphrase else key_material
    if page_num == 1:
        encrypted_input = plain_page[SALT_SIZE: PAGE_SIZE - RESERVE_SIZE]
        prefix = salt
    else:
        encrypted_input = plain_page[: PAGE_SIZE - RESERVE_SIZE]
        prefix = b""

    cipher = Cipher(algorithms.AES(enc_key), modes.CBC(iv), backend=default_backend())
    encryptor = cipher.encryptor()
    encrypted = encryptor.update(encrypted_input) + encryptor.finalize()

    page_without_hmac = prefix + encrypted + iv
    mac = hmac.new(_derive_mac_key(enc_key, salt), digestmod=hashlib.sha512)
    mac.update(page_without_hmac[SALT_SIZE if page_num == 1 else 0:])
    mac.update(page_num.to_bytes(4, "little"))
    return page_without_hmac + mac.digest()


def _decrypt_sample(key_hex: str, encrypted_db: bytes, monkeypatch) -> bytes:
    with tempfile.TemporaryDirectory() as tmpdir:
        src = Path(tmpdir) / "source.db"
        dst = Path(tmpdir) / "out.db"
        src.write_bytes(encrypted_db)
        monkeypatch.setattr(wechat_decrypt, "collect_sqlite_diagnostics", lambda *args, **kwargs: {"quick_check_ok": True})
        monkeypatch.setattr(wechat_decrypt, "sqlite_diagnostics_status", lambda diagnostics: "ok")
        decryptor = WeChatDatabaseDecryptor(key_hex)
        assert decryptor.decrypt_database(str(src), str(dst))
        return dst.read_bytes()


def test_decrypt_database_accepts_raw_enc_key_like_weflow(monkeypatch):
    raw_key = bytes.fromhex("00112233445566778899aabbccddeefffedcba98765432100123456789abcdef")
    salt = bytes.fromhex("50f4090ef6897e146f94109f13743e34")
    page1 = _build_plain_page(0x41, first_page=True)
    page2 = _build_plain_page(0x42, first_page=False)

    encrypted_db = _encrypt_page(raw_key, page1, 1, salt, bytes.fromhex("0102030405060708090a0b0c0d0e0f10"))
    encrypted_db += _encrypt_page(raw_key, page2, 2, salt, bytes.fromhex("1112131415161718191a1b1c1d1e1f20"))

    assert _decrypt_sample(raw_key.hex(), encrypted_db, monkeypatch) == page1 + page2


def test_decrypt_database_keeps_sqlcipher_passphrase_compatibility(monkeypatch):
    passphrase_key = bytes.fromhex("9f5dd0d3b6d0477ea5045c9e380ee272e53927993eb548dd98a022e842d5f7bd")
    salt = bytes.fromhex("40f4090ef6897e146f94109f13743e34")
    page1 = _build_plain_page(0x51, first_page=True)
    page2 = _build_plain_page(0x52, first_page=False)

    encrypted_db = _encrypt_page(passphrase_key, page1, 1, salt, bytes.fromhex("2122232425262728292a2b2c2d2e2f30"), passphrase=True)
    encrypted_db += _encrypt_page(passphrase_key, page2, 2, salt, bytes.fromhex("3132333435363738393a3b3c3d3e3f40"), passphrase=True)

    assert _decrypt_sample(passphrase_key.hex(), encrypted_db, monkeypatch) == page1 + page2


def test_decrypt_database_keeps_page_when_non_first_hmac_mismatch(monkeypatch):
    raw_key = bytes.fromhex("00112233445566778899aabbccddeefffedcba98765432100123456789abcdef")
    salt = bytes.fromhex("60f4090ef6897e146f94109f13743e34")
    page1 = _build_plain_page(0x61, first_page=True)
    page2 = _build_plain_page(0x62, first_page=False)

    encrypted_db = _encrypt_page(raw_key, page1, 1, salt, bytes.fromhex("4142434445464748494a4b4c4d4e4f50"))
    encrypted_page2 = bytearray(_encrypt_page(raw_key, page2, 2, salt, bytes.fromhex("5152535455565758595a5b5c5d5e5f60")))
    encrypted_page2[-1] ^= 0x01  # 只破坏 HMAC，不破坏密文和 IV。
    encrypted_db += bytes(encrypted_page2)

    with tempfile.TemporaryDirectory() as tmpdir:
        src = Path(tmpdir) / "source.db"
        dst = Path(tmpdir) / "out.db"
        src.write_bytes(encrypted_db)
        monkeypatch.setattr(wechat_decrypt, "collect_sqlite_diagnostics", lambda *args, **kwargs: {"quick_check_ok": True})
        monkeypatch.setattr(wechat_decrypt, "sqlite_diagnostics_status", lambda diagnostics: "ok")

        decryptor = WeChatDatabaseDecryptor(raw_key.hex())
        assert decryptor.decrypt_database(str(src), str(dst))
        assert dst.read_bytes() == page1 + page2
        assert decryptor.last_result["failed_pages"] == 0
        assert decryptor.last_result["hmac_warning_pages"] == 1
