#!/usr/bin/env bash
set -euo pipefail

# ─────────────────────────────────────────────────────────────────────────────
# test-census.sh — Smoke test for certisigma-census in ephemeral Docker container
#
# PURPOSE:
#   Verifies the *installed* certisigma-census package works end-to-end in a
#   clean Python environment: imports, CLI entrypoint, scan, manifest, compare,
#   retry module, and live API smoke tests (public endpoints only).
#
#   This is NOT a substitute for pytest unit tests (tests/). This script
#   catches installation issues, missing dependencies, and entrypoint
#   registration — the kind of bugs that only surface in a fresh virtualenv.
#
# WHEN TO RUN:
#   - Before publishing:  ./scripts/test-census.sh --local
#   - After publishing:   ./scripts/test-census.sh 0.3.1
#   - Quick regression:   ./scripts/test-census.sh --local
#
# USAGE:
#   ./scripts/test-census.sh              # latest from PyPI
#   ./scripts/test-census.sh 0.2.0        # pinned version from PyPI
#   ./scripts/test-census.sh --local      # local source (editable install)
#
# TEST COVERAGE:
#    1. Imports & version
#    2. Manifest CRUD (SQLite backend, save/load, round-trip)
#    3. Scanner & Hashing (walk_files, hash computation)
#    4. Compare module (data structures, hash-to-path mapping)
#    5. Retry module (import, backoff logic)
#    6. Phase 2 features (filters, resume, CSV/JSON export, logging, merge)
#    7. Phase 3 features (SQLite manifest, JSON migration, watch imports)
#    8. End-to-end: scan --dry-run → status
#    9. Live API smoke (health, verify — public endpoints)
# ─────────────────────────────────────────────────────────────────────────────

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CONTAINER_NAME="census-smoke-test"
IMAGE_NAME="census-smoke-test:latest"
VERSION="${1:-latest}"
EXIT_CODE=0

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'

header()  { echo -e "\n${BLUE}══════════════════════════════════════════════════════════════${NC}"; echo -e "${BLUE}  $1${NC}"; echo -e "${BLUE}══════════════════════════════════════════════════════════════${NC}\n"; }
section() { echo -e "\n${CYAN}  ── $1 ──${NC}\n"; }
pass()    { echo -e "  ${GREEN}✓${NC} $1"; }
fail()    { echo -e "  ${RED}✗${NC} $1"; EXIT_CODE=1; }

cleanup() {
    echo -e "\n${YELLOW}Cleaning up...${NC}"
    docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
    docker rmi "$IMAGE_NAME" 2>/dev/null || true
}
trap cleanup EXIT

header "CertiSigma Census — Smoke Test Suite"
echo "  Version:  $VERSION"
echo "  Date:     $(date -Iseconds)"

# ─── Build ephemeral test image ──────────────────────────────────────────────
header "Building ephemeral test container"

docker build -q -t "$IMAGE_NAME" -f - "$PROJECT_DIR" <<'DOCKERFILE'
FROM python:3.12-slim
RUN python -m pip install --no-cache-dir --upgrade pip
WORKDIR /test
COPY . /census-src/
CMD ["bash"]
DOCKERFILE
pass "Test image built"

# ─── Install Census ──────────────────────────────────────────────────────────
if [ "$VERSION" = "--local" ]; then
    INSTALL_CMD="pip install --no-cache-dir '/census-src[dev,watch]'"
    MODE="local source"
elif [ "$VERSION" = "latest" ]; then
    INSTALL_CMD="pip install --no-cache-dir 'certisigma-census[watch]'"
    MODE="latest from PyPI"
else
    INSTALL_CMD="pip install --no-cache-dir 'certisigma-census[watch]==$VERSION'"
    MODE="v$VERSION from PyPI"
fi

header "Installing certisigma-census ($MODE)"
docker run -d --name "$CONTAINER_NAME" "$IMAGE_NAME" sleep 600 >/dev/null
docker exec "$CONTAINER_NAME" bash -c "$INSTALL_CMD" 2>&1 | tail -3
pass "certisigma-census installed"

# ─── Write test script into container ────────────────────────────────────────
docker exec "$CONTAINER_NAME" tee /test/test_smoke.py > /dev/null << 'PYEOF'
"""Census smoke tests — run in clean Docker environment."""

import sys
import os
import json
import tempfile
import time
from pathlib import Path

failures = []
total = [0]

def check(name, condition):
    total[0] += 1
    if condition:
        print(f'  PASS: {name}')
    else:
        print(f'  FAIL: {name}')
        failures.append(name)

def expect_error(name, exc_type, fn, *args, **kwargs):
    total[0] += 1
    try:
        fn(*args, **kwargs)
        print(f'  FAIL: {name} (no exception)')
        failures.append(name)
    except exc_type:
        print(f'  PASS: {name}')
    except Exception as e:
        print(f'  FAIL: {name} (got {type(e).__name__}: {e})')
        failures.append(name)

# ═══════════════════════════════════════════════════
print('\n  -- 1. Imports & Version --\n')
# ═══════════════════════════════════════════════════

import certisigma_census
check(f'census version is string ({certisigma_census.__version__})',
      isinstance(certisigma_census.__version__, str) and len(certisigma_census.__version__) >= 5)

from certisigma_census.cli import main
check('cli.main is callable', callable(main))

from certisigma_census.manifest import Manifest, ManifestEntry
check('manifest imports', True)

from certisigma_census.scanner import scan_directory, walk_files
check('scanner imports', True)

from certisigma_census.compare import compare_directory, CompareReport, MatchResult
check('compare imports', True)

from certisigma_census.retry import retry_api_call
check('retry imports', True)

import certisigma
check(f'certisigma SDK version ({certisigma.__version__})',
      isinstance(certisigma.__version__, str))

from certisigma import (
    CertiSigmaClient, hash_file, hash_bytes,
    CertiSigmaError, AuthenticationError, RateLimitError, QuotaExceededError,
)
check('SDK imports', True)

# ═══════════════════════════════════════════════════
print('\n  -- 2. Manifest CRUD --\n')
# ═══════════════════════════════════════════════════

m = Manifest(root_dir='/data/test')
for i in range(10):
    m.add(ManifestEntry(hash_hex=f'{i:064x}', path=f'file_{i}.txt', size=(i+1)*100, mtime=float(i)))
check('manifest add 10 entries', m.total == 10)
check('manifest attested_count == 0', m.attested_count == 0)

m.mark_attested(f'{0:064x}', 'att_1', '2026-01-01T00:00:00Z')
m.mark_attested(f'{1:064x}', 'att_2', '2026-01-02T00:00:00Z')
check('mark_attested', m.attested_count == 2)

entry = m.get(f'{0:064x}')
check('get entry', entry is not None and entry.attested and entry.attestation_id == 'att_1')

check('get missing returns None', m.get('ff' * 32) is None)

with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
    mpath = f.name
m.save(Path(mpath))
loaded = Manifest.load(Path(mpath))
check('save/load round-trip: total', loaded.total == 10)
check('save/load round-trip: attested', loaded.attested_count == 2)
check('save/load round-trip: root_dir', loaded.root_dir == '/data/test')
check('save/load round-trip: entry data', loaded.get(f'{0:064x}').attestation_id == 'att_1')
os.unlink(mpath)

# ═══════════════════════════════════════════════════
print('\n  -- 3. Scanner & Hashing --\n')
# ═══════════════════════════════════════════════════

KNOWN = b'CertiSigma Census test vector'
KNOWN_HASH_BYTES = hash_bytes(KNOWN)
check('hash_bytes returns 64-char hex', len(KNOWN_HASH_BYTES) == 64)
check('hash_bytes is lowercase hex', KNOWN_HASH_BYTES == KNOWN_HASH_BYTES.lower())

with tempfile.NamedTemporaryFile(delete=False, suffix='.bin') as f:
    f.write(KNOWN)
    tmp_file = f.name
check('hash_file matches hash_bytes', hash_file(tmp_file) == KNOWN_HASH_BYTES)
os.unlink(tmp_file)

check('hash_bytes(empty)', hash_bytes(b'') == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')

tmpdir = tempfile.mkdtemp()
for i in range(5):
    Path(tmpdir, f'doc_{i}.txt').write_text(f'content-{i}')
Path(tmpdir, '.hidden').write_text('hidden')
os.makedirs(os.path.join(tmpdir, '.git'))
Path(tmpdir, '.git', 'config').write_text('x')

files = list(walk_files(Path(tmpdir)))
names = [f.name for f in files]
check('walk_files finds 5 visible files', len(files) == 5)
check('walk_files skips .hidden', '.hidden' not in names)
check('walk_files skips .git/', 'config' not in names)

manifest = scan_directory(Path(tmpdir))
check('scan_directory total == 5', manifest.total == 5)
check('scan_directory root_dir', manifest.root_dir == str(Path(tmpdir).resolve()))
for e in manifest.entries.values():
    check(f'entry {e.path}: hash length', len(e.hash_hex) == 64)

import shutil
shutil.rmtree(tmpdir)

# ═══════════════════════════════════════════════════
print('\n  -- 4. Compare Module --\n')
# ═══════════════════════════════════════════════════

report = CompareReport()
check('CompareReport defaults', report.total_suspect == 0 and report.matched == 0)
check('CompareReport.matches is list', isinstance(report.matches, list))

mr = MatchResult(hash_hex='aa' * 32, suspect_path='leak.pdf', attestation_id='att_99', level='T1')
check('MatchResult fields', mr.hash_hex == 'aa' * 32 and mr.level == 'T1')
check('MatchResult.inventory_path default None', mr.inventory_path is None)

# ═══════════════════════════════════════════════════
print('\n  -- 5. Retry Module --\n')
# ═══════════════════════════════════════════════════

result = retry_api_call(lambda: 'ok', max_retries=0)
check('retry: immediate success', result == 'ok')

expect_error('retry: auth error not retried', AuthenticationError,
             retry_api_call, lambda: (_ for _ in ()).throw(AuthenticationError('bad key')),
             max_retries=3)

expect_error('retry: quota error not retried', QuotaExceededError,
             retry_api_call, lambda: (_ for _ in ()).throw(QuotaExceededError('quota')),
             max_retries=3)

call_count = [0]
def _fail_then_succeed():
    call_count[0] += 1
    if call_count[0] < 3:
        raise CertiSigmaError('server error', status_code=500)
    return 'recovered'

import unittest.mock
with unittest.mock.patch('certisigma_census.retry.time.sleep'):
    result = retry_api_call(_fail_then_succeed, max_retries=3, base_delay=0.01)
check('retry: recovers after 500 errors', result == 'recovered' and call_count[0] == 3)

# ═══════════════════════════════════════════════════
print('\n  -- 6. Phase 2 Features --\n')
# ═══════════════════════════════════════════════════

from click.testing import CliRunner
import csv as csv_mod
from io import StringIO

runner = CliRunner()

# -- Filters --
filter_dir = tempfile.mkdtemp()
Path(filter_dir, 'report.pdf').write_bytes(b'pdf content here')
Path(filter_dir, 'data.csv').write_bytes(b'csv content here')
Path(filter_dir, 'debug.log').write_bytes(b'log content here')
Path(filter_dir, 'tiny.txt').write_bytes(b'x')
Path(filter_dir, 'medium.bin').write_bytes(b'x' * 500)

fmanifest = os.path.join(filter_dir, '..', 'filter-manifest.db')
r = runner.invoke(main, ['scan', filter_dir, '--dry-run', '--manifest', fmanifest,
                         '--include', '*.pdf', '--include', '*.csv'])
check('--include filters to matching files', r.exit_code == 0 and 'Found 2 files' in r.output)

r2 = runner.invoke(main, ['scan', filter_dir, '--dry-run', '--manifest', fmanifest,
                          '--exclude', '*.log'])
check('--exclude removes matching files', r2.exit_code == 0 and 'Found 4 files' in r2.output)

r3 = runner.invoke(main, ['scan', filter_dir, '--dry-run', '--manifest', fmanifest,
                          '--min-size', '100', '--max-size', '1K'])
check('--min-size/--max-size filters by size', r3.exit_code == 0 and 'Found 1 files' in r3.output)

# -- Resume --
resume_dir = tempfile.mkdtemp()
for i in range(3):
    Path(resume_dir, f'file_{i}.txt').write_bytes(f'content-{i}'.encode())
resume_manifest = os.path.join(resume_dir, '..', 'resume-manifest.db')

r4 = runner.invoke(main, ['scan', resume_dir, '--dry-run', '--manifest', resume_manifest])
check('first scan for resume', r4.exit_code == 0 and 'Found 3 files' in r4.output)

Path(resume_dir, 'file_3.txt').write_bytes(b'new file')
r5 = runner.invoke(main, ['scan', resume_dir, '--dry-run', '--resume', '--manifest', resume_manifest])
check('--resume picks up new files', r5.exit_code == 0 and 'Resuming' in r5.output and 'Found 4 files' in r5.output)

# -- Export CSV --
export_csv_path = os.path.join(resume_dir, '..', 'export.csv')
r6 = runner.invoke(main, ['export', resume_manifest, '--format', 'csv', '--output', export_csv_path])
check('export --format csv exits 0', r6.exit_code == 0)
with open(export_csv_path, newline='') as f:
    rows = list(csv_mod.DictReader(f))
check('export CSV has correct rows', len(rows) == 4)
check('export CSV has hash_hex column', 'hash_hex' in rows[0])

# -- Export JSON --
export_json_path = os.path.join(resume_dir, '..', 'export.json')
r7 = runner.invoke(main, ['export', resume_manifest, '--format', 'json', '--output', export_json_path])
check('export --format json exits 0', r7.exit_code == 0)
export_data = json.loads(Path(export_json_path).read_text())
check('export JSON has entries', len(export_data.get('entries', [])) == 4)

# -- JSON logging --
r8 = runner.invoke(main, ['-v', '--log-format', 'json', 'status', resume_manifest])
check('--log-format json accepted', r8.exit_code == 0)

# -- _parse_size --
from certisigma_census.cli import _parse_size
check('_parse_size("10K") == 10240', _parse_size('10K') == 10240)
check('_parse_size("1.5G")', _parse_size('1.5G') == int(1.5 * 1024**3))

# -- Manifest.merge and has_path --
m1 = Manifest(root_dir='/tmp')
m1.add(ManifestEntry(hash_hex='aa'*32, path='a.txt', size=100, mtime=1.0))
m1.mark_attested('aa'*32, 'att_1', '2026-01-01')
m2 = Manifest(root_dir='/tmp')
m2.add(ManifestEntry(hash_hex='bb'*32, path='b.txt', size=200, mtime=2.0))
changed = m1.merge(m2)
check('Manifest.merge adds new entries', m1.total == 2 and changed == 1)
check('Manifest.merge preserves attestation', m1.get('aa'*32).attested is True)
check('Manifest.has_path finds entry', m1.has_path('a.txt') is not None)
check('Manifest.has_path returns None for missing', m1.has_path('z.txt') is None)

shutil.rmtree(filter_dir)
shutil.rmtree(resume_dir)
for p in [fmanifest, resume_manifest, export_csv_path, export_json_path]:
    for ext in ['', '.db', '.json', '.json.bak']:
        pp = p + ext if ext and not p.endswith(ext) else p
        if os.path.exists(pp):
            os.unlink(pp)

# ═══════════════════════════════════════════════════
print('\n  -- 7. Phase 3 Features --\n')
# ═══════════════════════════════════════════════════

import sqlite3

# SQLite manifest format
sm = Manifest(root_dir='/test/sqlite')
sm.add(ManifestEntry(hash_hex='cc'*32, path='doc.pdf', size=1024, mtime=1.0))
sm_path = Path(tempfile.mktemp(suffix='.db'))
sm.save(sm_path)
check('manifest saves as SQLite', sm_path.read_bytes()[:6] == b'SQLite')

conn = sqlite3.connect(str(sm_path))
uv = conn.execute('PRAGMA user_version').fetchone()[0]
conn.close()
check('schema version = 2', uv == 2)

sm_loaded = Manifest.load(sm_path)
check('SQLite load: total', sm_loaded.total == 1)
check('SQLite load: root_dir', sm_loaded.root_dir == '/test/sqlite')
os.unlink(str(sm_path))

# New manifest API methods
nm = Manifest(root_dir='/tmp')
nm.add(ManifestEntry(hash_hex='dd'*32, path='x.txt', size=10, mtime=1.0))
nm.add(ManifestEntry(hash_hex='ee'*32, path='y.txt', size=20, mtime=2.0))
nm.mark_attested('dd'*32, 'att_d', '2026-01-01')
check('unattested_hashes()', nm.unattested_hashes() == ['ee'*32])
check('iter_entries()', len(list(nm.iter_entries())) == 2)
check('remove_by_path()', nm.remove_by_path('x.txt') is True and nm.total == 1)

# JSON migration
json_legacy = {
    'version': '1', 'created_at': 1700000000.0, 'updated_at': 1700000001.0,
    'root_dir': '/data/legacy',
    'entries': {
        'ff'*32: {'hash_hex': 'ff'*32, 'path': 'old.txt', 'size': 42, 'mtime': 1.0,
                  'attested': True, 'attestation_id': 'att_legacy', 'attested_at': '2025-06-01'},
    },
}
json_p = Path(tempfile.mktemp(suffix='.json'))
json_p.write_text(json.dumps(json_legacy, default=str))
migrated = Manifest.load(json_p)
check('JSON migration: total', migrated.total == 1)
check('JSON migration: attestation preserved', migrated.get('ff'*32).attested is True)
check('JSON migration: .db created', json_p.with_suffix('.db').exists())
check('JSON migration: .json renamed to .bak', json_p.with_suffix('.json.bak').exists())
for p in [json_p.with_suffix('.db'), json_p.with_suffix('.json.bak')]:
    if p.exists():
        os.unlink(str(p))

# Manifest.close()
cm = Manifest(root_dir='/tmp')
cm.add(ManifestEntry(hash_hex='11'*32, path='close_test.txt', size=10, mtime=1.0))
cm.close()
try:
    _ = cm.total
    check('Manifest.close() prevents operations', False)
except Exception:
    check('Manifest.close() prevents operations', True)

# Manifest.load() error path
try:
    Manifest.load(Path('/nonexistent/manifest.db'))
    check('Manifest.load() missing file raises', False)
except FileNotFoundError:
    check('Manifest.load() missing file raises', True)

# _resolve_manifest_path
from certisigma_census.cli import _resolve_manifest_path
resolve_dir = tempfile.mkdtemp()
rm = Manifest(root_dir='/tmp')
rm.add(ManifestEntry(hash_hex='22'*32, path='r.txt', size=10, mtime=1.0))
rm.save(Path(resolve_dir) / 'test.json')
resolved = _resolve_manifest_path(Path(resolve_dir) / 'test.json')
check('_resolve_manifest_path: .json -> .db', resolved == Path(resolve_dir) / 'test.db')
resolved2 = _resolve_manifest_path(Path(resolve_dir) / 'test.db')
check('_resolve_manifest_path: .db direct', resolved2 == Path(resolve_dir) / 'test.db')
resolved3 = _resolve_manifest_path(Path(resolve_dir) / 'nonexistent.db')
check('_resolve_manifest_path: missing returns original', resolved3 == Path(resolve_dir) / 'nonexistent.db')
shutil.rmtree(resolve_dir)

# Watch module imports
try:
    from certisigma_census.watcher import CensusEventHandler, EventType, watch_directory
    check('watcher imports', True)
except ImportError as e:
    check(f'watcher imports ({e})', False)

r_watch_help = runner.invoke(main, ['watch', '--help'])
check('census watch --help exits 0', r_watch_help.exit_code == 0)
check('watch --help shows --debounce', '--debounce' in r_watch_help.output)
check('watch --help shows --polling', '--polling' in r_watch_help.output)
check('watch --help shows --on-delete', '--on-delete' in r_watch_help.output)

# ═══════════════════════════════════════════════════
print('\n  -- 8. Phase 3.5 Features (Evidence + Reports) --\n')
# ═══════════════════════════════════════════════════

# Evidence module imports
from certisigma_census.evidence import verify_hash, integrity_check, VerifyDetail, IntegrityReport, ModifiedFile
check('evidence imports', True)

# Report module imports
from certisigma_census.report import generate_html_report, generate_pdf_report, generate_evidence_bundle
check('report imports', True)

# CLI commands
r_verify = runner.invoke(main, ['verify', '--help'])
check('census verify --help exits 0', r_verify.exit_code == 0)
check('verify --help shows --file', '--file' in r_verify.output)
check('verify --help shows --save-ots', '--save-ots' in r_verify.output)

r_integrity = runner.invoke(main, ['integrity', '--help'])
check('census integrity --help exits 0', r_integrity.exit_code == 0)
check('integrity --help shows --strict', '--strict' in r_integrity.output)

r_report = runner.invoke(main, ['report', '--help'])
check('census report --help exits 0', r_report.exit_code == 0)
check('report --help shows --evidence', '--evidence' in r_report.output)
check('report --help shows --bundle', '--bundle' in r_report.output)

# HTML report generation
report_dir = tempfile.mkdtemp()
for i in range(3):
    Path(report_dir, f'file_{i}.txt').write_bytes(f'content-{i}'.encode())
report_manifest = os.path.join(report_dir, '..', 'report-test.db')
r_scan = runner.invoke(main, ['scan', report_dir, '--dry-run', '--manifest', report_manifest])
check('scan for report test', r_scan.exit_code == 0)

html_out = os.path.join(report_dir, '..', 'test-report.html')
r_html = runner.invoke(main, ['report', report_manifest, '-o', html_out])
check('census report HTML exits 0', r_html.exit_code == 0)
check('HTML report file exists', os.path.exists(html_out))
html_content = Path(html_out).read_text()
check('HTML report valid', '<!DOCTYPE html>' in html_content and 'CertiSigma Census' in html_content)

# PDF report generation
pdf_out = os.path.join(report_dir, '..', 'test-report.pdf')
r_pdf = runner.invoke(main, ['report', report_manifest, '-o', pdf_out])
check('census report PDF exits 0', r_pdf.exit_code == 0)
check('PDF report file exists', os.path.exists(pdf_out))
check('PDF report valid header', Path(pdf_out).read_bytes()[:5] == b'%PDF-')

# Integrity check
r_integ = runner.invoke(main, ['integrity', report_manifest])
check('census integrity exits 0', r_integ.exit_code == 0)
check('integrity output contains OK', 'INTEGRITY OK' in r_integ.output or 'Unchanged' in r_integ.output)

# Cleanup
shutil.rmtree(report_dir)
for p in [report_manifest, html_out, pdf_out]:
    for ext in ['', '.db']:
        pp = p + ext if ext and not p.endswith(ext) else p
        if os.path.exists(pp):
            os.unlink(pp)

# ═══════════════════════════════════════════════════
print('\n  -- 9. Phase 4 Features (Diff, Hash, Config, Completion) --\n')
# ═══════════════════════════════════════════════════

# Diff module imports
from certisigma_census.diff import diff_manifests, DiffResult, DiffEntry, generate_diff_html
check('diff imports', True)

# Config module imports
from certisigma_census.config import load_config, mask_sensitive, init_config, config_paths
check('config imports', True)

# CLI commands
r_diff = runner.invoke(main, ['diff', '--help'])
check('census diff --help exits 0', r_diff.exit_code == 0)
check('diff --help shows --json', '--json' in r_diff.output)
check('diff --help shows --summary', '--summary' in r_diff.output)

r_hash = runner.invoke(main, ['hash', '--help'])
check('census hash --help exits 0', r_hash.exit_code == 0)
check('hash --help shows --verify', '--verify' in r_hash.output)

r_track = runner.invoke(main, ['track', '--help'])
check('census track --help exits 0', r_track.exit_code == 0)
check('track --help shows --poll', '--poll' in r_track.output)

r_config = runner.invoke(main, ['config', '--help'])
check('census config --help exits 0', r_config.exit_code == 0)
check('config --help shows show/init', 'show' in r_config.output and 'init' in r_config.output)

r_completion = runner.invoke(main, ['completion', '--help'])
check('census completion --help exits 0', r_completion.exit_code == 0)
check('completion --help shows bash/zsh/fish', 'bash' in r_completion.output)

# Diff functionality
diff_dir = tempfile.mkdtemp()
for i in range(3):
    Path(diff_dir, f'file_{i}.txt').write_bytes(f'content-{i}'.encode())
diff_m1 = os.path.join(diff_dir, '..', 'diff-base.db')
r_d1 = runner.invoke(main, ['scan', diff_dir, '--dry-run', '--manifest', diff_m1])
check('scan for diff base', r_d1.exit_code == 0)

Path(diff_dir, 'file_3.txt').write_bytes(b'new file')
Path(diff_dir, 'file_0.txt').write_bytes(b'modified content')
diff_m2 = os.path.join(diff_dir, '..', 'diff-target.db')
r_d2 = runner.invoke(main, ['scan', diff_dir, '--dry-run', '--manifest', diff_m2])
check('scan for diff target', r_d2.exit_code == 0)

r_diff_run = runner.invoke(main, ['diff', diff_m1, diff_m2])
check('census diff exits with changes', r_diff_run.exit_code != 0)
check('census diff shows Added', 'Added' in r_diff_run.output)

r_diff_json = runner.invoke(main, ['diff', diff_m1, diff_m2, '--json'])
diff_data = json.loads(r_diff_json.output)
check('census diff --json valid', 'added' in diff_data and 'removed' in diff_data)

# Hash functionality
hash_file = os.path.join(diff_dir, 'file_1.txt')
r_hash_run = runner.invoke(main, ['hash', hash_file])
check('census hash exits 0', r_hash_run.exit_code == 0)
check('census hash outputs 64 hex chars', len(r_hash_run.output.split()[0]) == 64)

# Config functionality
r_config_show = runner.invoke(main, ['config', 'show'])
check('census config show exits 0', r_config_show.exit_code == 0)
config_data = json.loads(r_config_show.output)
check('config show has api_key', 'api_key' in config_data)

r_config_paths = runner.invoke(main, ['config', 'paths'])
check('census config paths exits 0', r_config_paths.exit_code == 0)

# Completion
r_comp_bash = runner.invoke(main, ['completion', 'bash'])
check('census completion bash exits 0', r_comp_bash.exit_code == 0)

shutil.rmtree(diff_dir)
for p in [diff_m1, diff_m2]:
    if os.path.exists(p):
        os.unlink(p)

# ═══════════════════════════════════════════════════
print('\n  -- 10. End-to-End: Dry Run → Status --\n')
# ═══════════════════════════════════════════════════

version_result = runner.invoke(main, ['--version'])
check('census --version exits 0', version_result.exit_code == 0)
check('census --version output', 'certisigma-census' in version_result.output)

help_result = runner.invoke(main, ['--help'])
check('census --help exits 0', help_result.exit_code == 0)
check('census --help lists commands', 'scan' in help_result.output and 'compare' in help_result.output)
check('census --help lists export', 'export' in help_result.output)
check('census --help lists watch', 'watch' in help_result.output)
check('census --help lists verify', 'verify' in help_result.output)
check('census --help lists integrity', 'integrity' in help_result.output)
check('census --help lists report', 'report' in help_result.output)
check('census --help lists diff', 'diff' in help_result.output)
check('census --help lists hash', 'hash' in help_result.output)
check('census --help lists track', 'track' in help_result.output)
check('census --help lists config', 'config' in help_result.output)
check('census --help lists completion', 'completion' in help_result.output)

e2e_dir = tempfile.mkdtemp()
for i in range(8):
    Path(e2e_dir, f'asset_{i}.bin').write_bytes(os.urandom(256))
manifest_file = os.path.join(e2e_dir, '..', 'e2e-manifest.db')

scan_result = runner.invoke(main, ['scan', e2e_dir, '--dry-run', '--manifest', manifest_file])
check('scan --dry-run exits 0', scan_result.exit_code == 0)
check('scan --dry-run creates manifest', os.path.exists(manifest_file))
check('scan --dry-run output', 'Found 8 files' in scan_result.output and 'Dry run' in scan_result.output)

status_result = runner.invoke(main, ['status', manifest_file])
check('status exits 0', status_result.exit_code == 0)
check('status shows total', 'Total:    8' in status_result.output)
check('status shows attested 0', 'Attested: 0' in status_result.output)
check('status shows pending 8', 'Pending:  8' in status_result.output)

loaded_m = Manifest.load(Path(manifest_file))
check('manifest SQLite has 8 entries', loaded_m.total == 8)
check('manifest SQLite has root_dir', loaded_m.root_dir.endswith(os.path.basename(e2e_dir)))

shutil.rmtree(e2e_dir)
if os.path.exists(manifest_file):
    os.unlink(manifest_file)

# ═══════════════════════════════════════════════════
print('\n  -- 11. Live API Smoke (public endpoints) --\n')
# ═══════════════════════════════════════════════════

try:
    client = CertiSigmaClient()
    hr = client.health()
    check('health() returns dict with status', isinstance(hr, dict) and 'status' in hr)
except Exception as e:
    check(f'health() ({type(e).__name__}: {e})', False)

try:
    vr = client.verify('0' * 64)
    check('verify(zeros) returns VerifyResult', hasattr(vr, 'exists'))
    check('verify(zeros).exists == False', vr.exists is False)
except Exception as e:
    check(f'verify() ({type(e).__name__}: {e})', False)

try:
    bvr = client.batch_verify(['0' * 64, '1' * 64])
    check('batch_verify returns BatchVerifyResult', hasattr(bvr, 'count'))
    check('batch_verify.count == 2', bvr.count == 2)
except Exception as e:
    check(f'batch_verify() ({type(e).__name__}: {e})', False)

# ═══════════════════════════════════════════════════
print()
if failures:
    print(f'CENSUS SMOKE: FAILED {len(failures)}/{total[0]}')
    for f in failures: print(f'  - {f}')
    sys.exit(1)
else:
    print(f'CENSUS SMOKE: ALL {total[0]} TESTS PASSED')
PYEOF

# ─── Run smoke tests ─────────────────────────────────────────────────────────
header "Census — Smoke Test Suite"

docker exec "$CONTAINER_NAME" python3 /test/test_smoke.py 2>&1
PY_EXIT=$?
if [ $PY_EXIT -eq 0 ]; then pass "Census smoke tests passed"; else fail "Census smoke tests FAILED"; fi

# ─── CLI entrypoint verification ─────────────────────────────────────────────
header "CLI Entrypoint Verification"

CENSUS_VER=$(docker exec "$CONTAINER_NAME" census --version 2>&1 || true)
echo "  census --version: $CENSUS_VER"
if echo "$CENSUS_VER" | grep -q "certisigma-census"; then
    pass "CLI entrypoint registered correctly"
else
    fail "CLI entrypoint not found or broken"
fi

CENSUS_HELP=$(docker exec "$CONTAINER_NAME" census --help 2>&1 || true)
if echo "$CENSUS_HELP" | grep -q "scan"; then
    pass "CLI shows scan command"
else
    fail "CLI missing scan command"
fi

# ─── Version & Registry Check ────────────────────────────────────────────────
header "Version & Registry Check"

CENSUS_VER=$(docker exec "$CONTAINER_NAME" python3 -c "import certisigma_census; print(certisigma_census.__version__)")
SDK_VER=$(docker exec "$CONTAINER_NAME" python3 -c "import certisigma; print(certisigma.__version__)")

echo "  Census:     $CENSUS_VER"
echo "  SDK:        $SDK_VER"
pass "Census v$CENSUS_VER using SDK v$SDK_VER"

if [ "$VERSION" != "--local" ] && [ "$VERSION" != "latest" ]; then
    if [ "$CENSUS_VER" = "$VERSION" ]; then
        pass "Installed version matches requested ($VERSION)"
    else
        fail "Version mismatch: installed=$CENSUS_VER requested=$VERSION"
    fi
fi

if [ "$VERSION" != "--local" ]; then
    PYPI_VER=$(docker exec "$CONTAINER_NAME" bash -c "pip index versions certisigma-census 2>/dev/null | head -1 | grep -oP '\([\d.]+\)' | tr -d '()'" 2>/dev/null || echo "?")
    echo "  PyPI:       $PYPI_VER"
    if [ "$PYPI_VER" != "?" ] && [ "$CENSUS_VER" = "$PYPI_VER" ]; then
        pass "Installed version matches PyPI ($PYPI_VER)"
    elif [ "$PYPI_VER" = "?" ]; then
        pass "PyPI version check skipped (not published yet)"
    else
        fail "Census $CENSUS_VER != PyPI $PYPI_VER"
    fi
fi

# ─── Final summary ───────────────────────────────────────────────────────────
header "Final Summary"

if [ $EXIT_CODE -eq 0 ]; then
    echo -e "  ${GREEN}ALL CENSUS SMOKE TESTS PASSED${NC}"
else
    echo -e "  ${RED}SOME TESTS FAILED${NC}"
fi
echo -e "  Census: $CENSUS_VER | SDK: $SDK_VER | Mode: $MODE"

exit $EXIT_CODE
