"""
AutoPOE2 - Kiểm thử hồi quy cho RE Pipeline & INV-BUILD-01 (Rule 11, 14)

Chạy 100% ở User Mode: KHÔNG cần quyền Admin, KHÔNG cần game đang chạy.

Các bất biến được khóa chặn:
  INV-BUILD-01  : tri thức offset phải khóa theo build fingerprint của client.
                  Tái hiện sự cố thật 10/09/2026: .data dịch -0x1000 giữa hai build
                  (.text VSize 0x2E840BE -> 0x2E83B6E) làm 8 static root RVA hardcode
                  lệch hoàn toàn và cả 4 AOB pattern [MISS].
  CROSS-TIER    : SectionLayoutHash của tầng Python PHẢI khớp byte-for-byte với
                  C++ ClientBuildId::SectionLayoutHash() (Rule 10 - zero drift).
                  Giá trị C++ đo được trong phiên 10/09/2026: 0x5c4322f5cdc0551a
                  (stdout của AutoPOE2_Tests.exe, Test 60).
  AUTHENTIC DATA: mọi số liệu đối chiếu đều lấy từ tệp PE thật / build record thật,
                  không dùng dữ liệu tự chế.
"""

import json
import sqlite3
import struct
import sys
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "tools" / "re_pipeline"))

import fingerprint as fp  # noqa: E402

BUILDS_DIR = REPO_ROOT / "data" / "builds"

# Giá trị C++ đo được thật trong phiên 10/09/2026 (AutoPOE2_Tests.exe, Test 60)
CPP_LAYOUT_HASH_POD = "0x5C4322F5CDC0551A"

# BUILD CŨ - số liệu THẬT trích từ bin/Release/memprobe_report.txt (5 phiên).
# TimeDateStamp KHÔNG được MemProbe ghi nhận -> để 0, không bịa (Rule 14).
OLD_BUILD_SECTIONS = [
    (".text", 0x00001000, 0x02E840BE, 0x60000020),
    (".rdata", 0x02E86000, 0x00B7C3A6, 0x40000040),
    (".data", 0x03A03000, 0x00D163DC, 0xC0000040),
]
OLD_BUILD_SIZE_OF_IMAGE = 0x4C9D000


def _make_build(stamp, size_of_image, sections, valid=True):
    """Dựng ClientBuildId từ số liệu section (dùng số liệu THẬT, không tự chế)."""
    build = fp.ClientBuildId(
        valid=valid,
        machine=fp.IMAGE_FILE_MACHINE_AMD64,
        number_of_sections=len(sections),
        time_date_stamp=stamp,
        size_of_image=size_of_image,
    )
    for name, va, vsize, chars in sections:
        build.sections.append(
            fp.SectionInfo(
                name=name,
                virtual_address=va,
                virtual_size=vsize,
                raw_size=vsize,
                characteristics=chars,
            )
        )
    return build


def test_fnv1a_known_answer():
    """FNV-1a 64-bit phải đúng với giá trị chuẩn đã công bố."""
    assert fp.fnv1a(fp.FNV_OFFSET_BASIS, b"") == fp.FNV_OFFSET_BASIS
    # FNV-1a 64-bit của "a" = 0xAF63DC4C8601EC8C (giá trị chuẩn)
    assert fp.fnv1a(fp.FNV_OFFSET_BASIS, b"a") == 0xAF63DC4C8601EC8C
    # FNV-1a 64-bit của "foobar" = 0x85944171F73967E8 (giá trị chuẩn)
    assert fp.fnv1a(fp.FNV_OFFSET_BASIS, b"foobar") == 0x85944171F73967E8


def test_fnv1a_stays_within_uint64():
    """Băm không được tràn khỏi 64-bit (chốt chặn overflow)."""
    result = fp.fnv1a(fp.FNV_OFFSET_BASIS, b"x" * 10000)
    assert 0 <= result <= 0xFFFFFFFFFFFFFFFF


def test_load_pe_headers_missing_file_is_safe():
    """Tệp không tồn tại phải thất bại an toàn, không ném ngoại lệ."""
    build = fp.load_pe_headers(Path("Z:/khong_ton_tai/fake.exe"))
    assert build.valid is False
    assert build.error
    assert build.sections == []


def test_load_pe_headers_rejects_non_pe(tmp_path):
    """Tệp rác không phải PE phải bị từ chối (magic != 'MZ')."""
    junk = tmp_path / "junk.bin"
    junk.write_bytes(b"\x00" * 4096)
    build = fp.load_pe_headers(junk)
    assert build.valid is False
    assert "MZ" in build.error


def test_struct_packing_matches_cpp_layout():
    """Đảm bảo định dạng pack dùng cho hash khớp kích thước kiểu C++."""
    assert len(struct.pack("<H", 7)) == 2          # uint16_t numberOfSections
    assert len(struct.pack("<I", 0x4C9C000)) == 4  # uint32_t sizeOfImage


RE_PIPELINE_DIR = REPO_ROOT / "tools" / "re_pipeline"
sys.path.insert(0, str(RE_PIPELINE_DIR))

import build_registry as br  # noqa: E402

SMOKE_EXTRACTION = REPO_ROOT / "data" / "re_out" / "SMOKE" / "extraction.json"

# Đồng bộ với OffsetsStore::kForbiddenAbsoluteKeys (C++)
FORBIDDEN_KEYS = (
    "player_addr", "xyz_addr", "shield_addr", "mana_addr",
    "spirit_addr", "ingame_addr", "terrain_addr",
    "camera_addr", "fog_density_addr",
)


def test_build_registry_end_to_end(tmp_path):
    """Vòng đời đầy đủ: extraction.json -> SQLite + TOML build-keyed.

    Chứng minh 3 invariant trên ARTEFACT THẬT do pipeline sinh ra:
      INV-SIG-UNIQUE  : chỉ signature match_count==1 vào registry
      INV-BUILD-01    : TOML phải gắn khóa build fingerprint
      INV-OFFSET-01   : không có khóa heap tuyệt đối nào
    """
    if not SMOKE_EXTRACTION.is_file():
        pytest.skip("Chưa có smoke extraction (chạy re_extract.py --exe ... trước)")

    db = tmp_path / "poe2_offsets.db"
    toml = tmp_path / "offsets_TEST.toml"

    rc = br.build_registry(SMOKE_EXTRACTION, db, toml)
    assert rc == 0, "build_registry phải thành công"
    assert db.is_file(), "Phải sinh được SQLite registry"
    assert toml.is_file(), "Phải sinh được TOML registry"

    content = toml.read_text(encoding="utf-8")

    # INV-BUILD-01: TOML phải mang khóa build fingerprint
    assert "build_time_date_stamp = 0x" in content
    assert "build_section_layout_hash = 0x" in content

    # INV-OFFSET-01: không được có bất kỳ khóa heap tuyệt đối nào
    for line in content.splitlines():
        stripped = line.split("#")[0].strip()
        for forbidden in FORBIDDEN_KEYS:
            assert not stripped.startswith(forbidden + " "), (
                f"INV-OFFSET-01 VI PHẠM: '{forbidden}' xuất hiện trong registry TOML"
            )

    # Mọi signature unique TRONG IMAGE trong extraction phải xuất hiện nguyên văn trong TOML
    payload = json.loads(SMOKE_EXTRACTION.read_text(encoding="utf-8"))
    image_base = int(str(payload["result"]["image_base"]), 16)
    size_of_image = int(str(payload["provenance"]["size_of_image"]), 16)
    unique_sigs = [
        c["signature"]
        for c in payload["result"]["candidates"]
        if c.get("unique") and br.is_in_image_data_slot(c, image_base, size_of_image)
    ]
    assert unique_sigs, "Smoke extraction phải có ít nhất 1 signature unique"
    for signature in unique_sigs:
        assert signature in content, f"Signature unique bị mất trong TOML: {signature}"

    # SQLite: số signature phải khớp và INV-SIG-UNIQUE phải được đảm bảo
    conn = sqlite3.connect(str(db))
    try:
        rows = conn.execute(
            "SELECT build_id, COUNT(*) FROM signatures GROUP BY build_id"
        ).fetchall()
        assert rows, "SQLite phải có ít nhất 1 build với signature"
        build_id, sig_count = rows[0]
        assert sig_count == len(unique_sigs), "Số signature trong DB phải khớp extraction"
        bad = conn.execute(
            "SELECT COUNT(*) FROM signatures WHERE unique_ok != 1 OR match_count != 1"
        ).fetchone()[0]
        assert bad == 0, "INV-SIG-UNIQUE: mọi record phải match_count == 1"
        builds = conn.execute("SELECT file_sha256 FROM builds WHERE build_id = ?", (build_id,)).fetchall()
        assert builds and len(builds[0][0]) == 64, "Build record phải có SHA-256 đầy đủ"
    finally:
        conn.close()


def test_parse_real_windows_binary():
    """Parse một PE THẬT luôn có trên Windows (không cần game, không cần Admin)."""
    notepad = Path(r"C:\Windows\System32\notepad.exe")
    if not notepad.is_file():
        pytest.skip("notepad.exe không có trên hệ thống này")

    build = fp.load_pe_headers(notepad)
    assert build.valid, f"Parse notepad.exe thất bại: {build.error}"
    assert build.machine == fp.IMAGE_FILE_MACHINE_AMD64
    assert build.number_of_sections >= 3
    assert build.size_of_image > 0
    assert build.file_size == notepad.stat().st_size

    text = build.find_section(".text")
    assert text is not None
    assert text.executable is True
    assert text.writable is False
    assert build.find_section(".khongton") is None

    # Fingerprint phải ổn định giữa 2 lần đọc
    again = fp.load_pe_headers(notepad)
    assert build.same_build_as(again)
    assert build.section_layout_hash() == again.section_layout_hash()


def test_same_build_as_rejects_invalid():
    """Build không hợp lệ (valid=False) không bao giờ được coi là khớp."""
    sections = [(".text", 0x1000, 0x2000, 0x60000020)]
    good = _make_build(0x11111111, 0x10000, sections)
    bad = _make_build(0x11111111, 0x10000, sections, valid=False)
    assert good.same_build_as(good) is True
    assert good.same_build_as(bad) is False
    assert bad.same_build_as(good) is False


def test_inv_build_01_data_section_shift_changes_hash():
    """INV-BUILD-01 REPRODUCTION: .data dịch -0x1000 PHẢI làm đổi layout hash.

    Số liệu THẬT từ bin/Release/memprobe_report.txt (build cũ, 5 phiên) và từ
    data/builds/6A9E477A.json (build mới, đo 10/09/2026).
    """
    old_build = _make_build(0, OLD_BUILD_SIZE_OF_IMAGE, OLD_BUILD_SECTIONS)

    record_path = BUILDS_DIR / "6A9E477A.json"
    if not record_path.is_file():
        pytest.skip("Chưa có build record 6A9E477A.json")
    record = json.loads(record_path.read_text(encoding="utf-8"))

    new_sections = [
        (
            s["name"],
            int(s["virtual_address"], 16),
            int(s["virtual_size"], 16),
            int(s["characteristics"], 16),
        )
        for s in record["sections"]
    ]
    new_build = _make_build(
        int(record["time_date_stamp"], 16),
        int(record["size_of_image"], 16),
        new_sections,
    )

    # Đây chính là gốc rễ sự cố: hai build PHẢI được nhận diện là khác nhau
    assert old_build.section_layout_hash() != new_build.section_layout_hash(), (
        "INV-BUILD-01: layout hash phải khác nhau khi .data dịch -0x1000"
    )
    assert old_build.same_build_as(new_build) is False

    # Xác nhận lại đúng số liệu thực địa đã đo
    data_new = new_build.find_section(".data")
    data_old = old_build.find_section(".data")
    assert data_new is not None and data_old is not None
    assert data_old.virtual_address - data_new.virtual_address == 0x1000, (
        "Bằng chứng thực địa: .data phải dịch đúng -0x1000 giữa 2 build"
    )
    text_new = new_build.find_section(".text")
    text_old = old_build.find_section(".text")
    assert text_old.virtual_size - text_new.virtual_size == 0x550, (
        ".text co lại đúng 0x550 byte giữa 2 build"
    )


def test_build_record_matches_live_client_and_cpp_tier():
    """Đối chiếu 3 nguồn: build record JSON <-> client thật trên đĩa <-> tầng C++."""
    exe = fp.locate_poe2_executable()
    if exe is None:
        pytest.skip("Không tìm thấy PathOfExile.exe trên máy này")

    live = fp.load_pe_headers(exe)
    assert live.valid, f"Parse client thật thất bại: {live.error}"

    record_path = BUILDS_DIR / f"{live.short_id}.json"
    if not record_path.is_file():
        pytest.skip(
            f"Client đã đổi build ({live.short_id}); cần chạy lại "
            "`python tools/re_pipeline/fingerprint.py --hash "
            f"--json data/builds/{live.short_id}.json`"
        )

    record = json.loads(record_path.read_text(encoding="utf-8"))
    assert record["schema"] == "autopoe2.client_build_id/v1"
    assert record["build_id"] == live.short_id
    assert record["time_date_stamp"] == f"0x{live.time_date_stamp:08X}"
    assert record["size_of_image"] == f"0x{live.size_of_image:X}"
    assert record["file_size"] == live.file_size
    assert record["number_of_sections"] == live.number_of_sections
    assert record["section_layout_hash"] == f"0x{live.section_layout_hash():016X}"

    # CROSS-TIER: Python phải khớp giá trị C++ đã đo (Rule 10 - zero drift)
    if live.short_id == "6A9E477A":
        assert record["section_layout_hash"] == CPP_LAYOUT_HASH_POD, (
            "SectionLayoutHash tầng Python LỆCH với tầng C++ "
            f"(C++={CPP_LAYOUT_HASH_POD}, Python={record['section_layout_hash']})"
        )


def test_build_record_sha256_is_authentic():
    """SHA-256 trong build record phải do băm thật tệp client sinh ra."""
    exe = fp.locate_poe2_executable()
    if exe is None:
        pytest.skip("Không tìm thấy PathOfExile.exe trên máy này")

    live = fp.load_pe_headers(exe)
    assert live.valid, f"Parse client thật thất bại: {live.error}"

    record_path = BUILDS_DIR / f"{live.short_id}.json"
    if not record_path.is_file():
        pytest.skip(f"Chưa có build record {live.short_id}.json")

    record = json.loads(record_path.read_text(encoding="utf-8"))
    sha = record.get("file_sha256", "")
    assert len(sha) == 64, "SHA-256 phải là chuỗi hex 64 ký tự"
    assert all(c in "0123456789abcdef" for c in sha), "SHA-256 phải là hex thường"

    rebuilt = fp.load_pe_headers_with_hash(exe)
    assert rebuilt.valid
    assert rebuilt.file_sha256 == sha, (
        "SHA-256 trong build record phải khớp với tệp client thật trên đĩa"
    )
    assert rebuilt.same_build_as(live)


def test_in_image_filter_rejects_tdb_from_live_client_extract():
    """Extract thật 6A9E477A có slot tdb/TEB unique — registry PHẢI loại.

    Artefact: data/re_out/6A9E477A/extraction.json (PyGhidra 10/09/2026,
    PathOfExile.exe SHA-256 e0cde00d...7504). Slot tdb ff00000058 không nằm
    trong SizeOfImage=0x4C9C000 nên không được thành static_root/AOB TOML.
    """
    extraction = REPO_ROOT / "data" / "re_out" / "6A9E477A" / "extraction.json"
    if not extraction.is_file():
        pytest.skip("Chưa có extract đầy đủ 6A9E477A")

    payload = json.loads(extraction.read_text(encoding="utf-8"))
    assert payload["provenance"]["build_id"] == "6A9E477A"
    image_base = int(str(payload["result"]["image_base"]), 16)
    size_of_image = int(str(payload["provenance"]["size_of_image"]), 16)
    assert size_of_image == 0x4C9C000

    tdb_slots = [
        c for c in payload["result"]["candidates"] if c.get("block") == "tdb"
    ]
    assert tdb_slots, "Extract thật phải còn ghi nhận slot tdb (không xóa artefact)"
    for slot in tdb_slots:
        assert br.is_in_image_data_slot(slot, image_base, size_of_image) is False

    accepted = [
        c
        for c in payload["result"]["candidates"]
        if c.get("unique")
        and c.get("match_count") == 1
        and br.is_in_image_data_slot(c, image_base, size_of_image)
    ]
    assert accepted, "Phải còn signature UNIQUE nằm trong .data/.rdata"
    for record in accepted:
        rva = br.rva_from_address(record["address"], image_base)
        assert 0 <= rva < size_of_image
        assert record["block"] in br.IN_IMAGE_DATA_BLOCKS


def test_label_targets_assigns_ingame_and_rejects_heap_keys(tmp_path):
    """Live labeler UPDATE target InGameState theo RVA live 0x45C9C90.

    RVA lấy từ MemProbe label-registry PID 36104 (10/09/2026 14:00:17),
    ValidateInGameState + ResolvePlayer maxHP=1330. Không dùng fan-in cao
    0x45CF0A0 (LOCK ADD, không phải InGameState). Khóa heap cấm (player_addr)
    phải bị bỏ qua (INV-OFFSET-01).
    """
    sys.path.insert(0, str(REPO_ROOT / "tools" / "re_pipeline"))
    import label_targets as lt  # noqa: E402

    rva = "0x45C9C90"
    db = tmp_path / "poe2_offsets.db"
    conn = sqlite3.connect(str(db))
    conn.execute(
        """
        CREATE TABLE offsets (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            build_id TEXT NOT NULL,
            kind TEXT NOT NULL,
            target TEXT NOT NULL,
            payload TEXT NOT NULL,
            validator_id TEXT,
            source TEXT NOT NULL,
            discovered_at TEXT NOT NULL,
            last_verified TEXT,
            hit_count INTEGER DEFAULT 0,
            miss_count INTEGER DEFAULT 0
        )
        """
    )
    conn.execute(
        """
        INSERT INTO offsets (
            build_id, kind, target, payload, validator_id, source, discovered_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?)
        """,
        (
            "6A9E477A",
            "aob_pattern",
            "unreviewed",
            json.dumps({"pattern": "F0 48 01 1D", "rip_offset": 4, "target_rva": rva}),
            "ValidateInGameState",
            "static_re",
            "10/09/2026 13:15:06",
        ),
    )
    conn.commit()
    conn.close()

    report = {
        "schema": "autopoe2.live_label/v1",
        "generated_at": "10/09/2026 13:45:00",
        "build_id": "6A9E477A",
        "entity_map_offset": "0x3E8",
        "labels": [
            {
                "target": "InGameState",
                "kind": "static_root",
                "index": 1,
                "rva": rva,
                "evidence": "ResolveInGameState+ResolvePlayer",
            },
            {
                "target": "player_addr",
                "kind": "heap",
                "index": 0,
                "rva": "0x8853EF06C",
                "evidence": "must-reject",
            },
        ],
    }
    report_path = tmp_path / "live_label_6A9E477A.json"
    report_path.write_text(json.dumps(report), encoding="utf-8")
    toml_path = tmp_path / "offsets_6A9E477A.toml"
    toml_path.write_text(
        "# test\nbuild_time_date_stamp = 0x6A9E477A\n"
        "static_root_1 = 0x45CF0A0\nstatic_root_2 = 0x45C9C90\n",
        encoding="utf-8",
    )

    rc = lt.apply_labels(report_path, db, toml_path)
    assert rc == 0

    conn = sqlite3.connect(str(db))
    rows = conn.execute("SELECT target, hit_count FROM offsets").fetchall()
    conn.close()
    assert ("InGameState", 1) in rows
    assert all(row[0] != "player_addr" for row in rows)
    text = toml_path.read_text(encoding="utf-8")
    assert "InGameState" in text
    assert "player_addr" not in text or "Bỏ qua" in text
    assert "entity_map_offset = 0x3E8" in text
    assert "static_root_1 = 0x45C9C90" in text
    assert text.count("# LIVE") == 1


