#!/usr/bin/env python3
"""
test_chunk_vault_positive.py - Positive Path Tests for ChunkVault CAS

Tests the happy path for Content-Addressed Storage:
- Store file
- Retrieve chunks
- Regenerate file
- Verify integrity

Run:
    pytest tests/test_chunk_vault_positive.py -v
"""

import hashlib
import os
import sys
import tempfile
from pathlib import Path

import pytest

# Add project root to path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))

from vaults.chunk_vault_hardened import (
    ChunkVaultHardened,
    FileManifest,
    create_hxz_seed,
    load_hxz_seed,
    sha256_bytes,
    sha256_file,
)


def sha256(data: bytes) -> str:
    """Helper to compute SHA256."""
    return hashlib.sha256(data).hexdigest()


class TestChunkVaultBasic:
    """Basic positive path tests."""

    def setup_method(self):
        """Set up temp directory for each test."""
        self.tmpdir = tempfile.mkdtemp()
        self.vault_path = Path(self.tmpdir) / "vault"
        self.vault = ChunkVaultHardened(str(self.vault_path), chunk_size=1024)

    def teardown_method(self):
        """Clean up temp directory."""
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def test_vault_creation(self):
        """Test vault directory is created."""
        assert self.vault_path.exists()
        assert (self.vault_path / ".tmp").exists()

    def test_store_small_file(self):
        """Test storing a small file."""
        # Create test file
        test_file = Path(self.tmpdir) / "test.bin"
        test_data = b"Hello, ChunkVault!" * 10
        test_file.write_bytes(test_data)

        # Store
        manifest = self.vault.store_file(str(test_file))

        # Verify manifest
        assert manifest.name == "test.bin"
        assert manifest.file_size == len(test_data)
        assert manifest.file_sha256 == sha256(test_data)
        assert len(manifest.chunks) == 1
        assert manifest.regen_capable is True

    def test_store_large_file(self):
        """Test storing a file larger than chunk size."""
        # Create test file (5 KB with 1 KB chunks = 5 chunks)
        test_file = Path(self.tmpdir) / "large.bin"
        test_data = os.urandom(5000)
        test_file.write_bytes(test_data)

        manifest = self.vault.store_file(str(test_file))

        assert manifest.file_size == 5000
        assert len(manifest.chunks) == 5
        assert manifest.file_sha256 == sha256(test_data)

        # Verify chunk sizes
        for i, chunk in enumerate(manifest.chunks):
            if i < 4:
                assert chunk['size'] == 1024
            else:
                assert chunk['size'] == 5000 - (4 * 1024)

    def test_regenerate_file(self):
        """Test regenerating a file from manifest."""
        # Store
        test_file = Path(self.tmpdir) / "original.bin"
        test_data = os.urandom(3000)
        test_file.write_bytes(test_data)

        manifest = self.vault.store_file(str(test_file))

        # Regenerate
        regen_file = Path(self.tmpdir) / "regenerated.bin"
        result = self.vault.regenerate(manifest, str(regen_file))

        assert result is True
        assert regen_file.exists()
        assert regen_file.read_bytes() == test_data

    def test_verify_regenerated_file(self):
        """Test verification of regenerated file."""
        test_file = Path(self.tmpdir) / "test.bin"
        test_data = os.urandom(2000)
        test_file.write_bytes(test_data)

        manifest = self.vault.store_file(str(test_file))

        # Regenerate
        regen_file = Path(self.tmpdir) / "regen.bin"
        self.vault.regenerate(manifest, str(regen_file))

        # Verify
        assert self.vault.verify(manifest, str(regen_file)) is True

    def test_verify_fails_for_modified_file(self):
        """Test verification fails for modified file."""
        test_file = Path(self.tmpdir) / "test.bin"
        test_data = os.urandom(1000)
        test_file.write_bytes(test_data)

        manifest = self.vault.store_file(str(test_file))

        # Modify the original
        test_file.write_bytes(test_data + b"MODIFIED")

        assert self.vault.verify(manifest, str(test_file)) is False

    def test_deduplication(self):
        """Test that identical chunks are deduplicated."""
        # Create two files with some identical content
        test_file1 = Path(self.tmpdir) / "file1.bin"
        test_file2 = Path(self.tmpdir) / "file2.bin"

        chunk_data = b"AAAA" * 256  # 1 KB of repeated A's
        test_file1.write_bytes(chunk_data)
        test_file2.write_bytes(chunk_data)

        manifest1 = self.vault.store_file(str(test_file1))
        manifest2 = self.vault.store_file(str(test_file2))

        # Both should have same chunk hash
        assert manifest1.chunks[0]['sha256'] == manifest2.chunks[0]['sha256']

        # Vault should only have one copy
        stats = self.vault.stats()
        assert stats['chunk_count'] == 1


class TestManifestValidation:
    """Test manifest validation."""

    def test_valid_manifest(self):
        """Test valid manifest passes validation."""
        manifest = FileManifest(
            name="test.bin",
            source_path="/tmp/test.bin",
            file_sha256="a" * 64,
            file_size=1000,
            algo="cas_v1_fixed_1024kb_sha256",
            chunk_size=1024,
            chunks=[
                {"i": 0, "sha256": "b" * 64, "size": 1000, "offset": 0}
            ],
            created="2026-01-01T00:00:00Z",
        )

        ok, err = manifest.validate()
        assert ok is True
        assert err == "ok"

    def test_invalid_file_sha256(self):
        """Test manifest with invalid file hash fails."""
        manifest = FileManifest(
            name="test.bin",
            source_path="/tmp/test.bin",
            file_sha256="not_a_valid_hash",
            file_size=1000,
            algo="cas_v1_fixed_1024kb_sha256",
            chunk_size=1024,
            chunks=[],
            created="2026-01-01T00:00:00Z",
        )

        ok, err = manifest.validate()
        assert ok is False
        assert "file_sha256" in err

    def test_invalid_algo(self):
        """Test manifest with unknown algo fails."""
        manifest = FileManifest(
            name="test.bin",
            source_path="/tmp/test.bin",
            file_sha256="a" * 64,
            file_size=1000,
            algo="unknown_compression_algo",
            chunk_size=1024,
            chunks=[],
            created="2026-01-01T00:00:00Z",
        )

        ok, err = manifest.validate()
        assert ok is False
        assert "Unknown algo" in err

    def test_chunk_size_mismatch(self):
        """Test manifest with mismatched chunk sizes fails."""
        manifest = FileManifest(
            name="test.bin",
            source_path="/tmp/test.bin",
            file_sha256="a" * 64,
            file_size=500,
            algo="cas_v1_fixed_1024kb_sha256",
            chunk_size=1024,
            chunks=[
                {"i": 0, "sha256": "b" * 64, "size": 1000, "offset": 0}
            ],
            created="2026-01-01T00:00:00Z",
        )

        ok, err = manifest.validate()
        assert ok is False
        assert "sum to" in err or "expected" in err.lower()


class TestHXZSeed:
    """Test HXZ seed creation and loading."""

    def setup_method(self):
        """Set up temp directory."""
        self.tmpdir = tempfile.mkdtemp()
        self.vault_path = Path(self.tmpdir) / "vault"
        self.vault = ChunkVaultHardened(str(self.vault_path), chunk_size=1024)

    def teardown_method(self):
        """Clean up."""
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def test_create_and_load_seed(self):
        """Test creating and loading HXZ seed."""
        # Store a file
        test_file = Path(self.tmpdir) / "test.bin"
        test_data = os.urandom(2000)
        test_file.write_bytes(test_data)

        manifest = self.vault.store_file(str(test_file))

        # Create seed
        seed_path = Path(self.tmpdir) / "test.hxz"
        create_hxz_seed(manifest, str(seed_path))

        assert seed_path.exists()
        assert seed_path.stat().st_size > 0

        # Load seed
        loaded = load_hxz_seed(str(seed_path))

        assert loaded.name == manifest.name
        assert loaded.file_sha256 == manifest.file_sha256
        assert loaded.file_size == manifest.file_size
        assert len(loaded.chunks) == len(manifest.chunks)

    def test_seed_is_gzipped(self):
        """Test seed file is gzip compressed."""
        test_file = Path(self.tmpdir) / "test.bin"
        test_file.write_bytes(b"X" * 1000)

        manifest = self.vault.store_file(str(test_file))
        seed_path = Path(self.tmpdir) / "test.hxz"
        create_hxz_seed(manifest, str(seed_path))

        # Check gzip magic bytes
        with open(seed_path, "rb") as f:
            magic = f.read(2)
        assert magic == b'\x1f\x8b'  # gzip magic


class TestIntegrityScan:
    """Test vault integrity scanning."""

    def setup_method(self):
        """Set up temp directory."""
        self.tmpdir = tempfile.mkdtemp()
        self.vault_path = Path(self.tmpdir) / "vault"
        self.vault = ChunkVaultHardened(str(self.vault_path), chunk_size=1024)

    def teardown_method(self):
        """Clean up."""
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def test_integrity_scan_clean_vault(self):
        """Test integrity scan on clean vault."""
        # Store some files
        for i in range(3):
            test_file = Path(self.tmpdir) / f"test{i}.bin"
            test_file.write_bytes(os.urandom(500))
            self.vault.store_file(str(test_file))

        # Scan
        results = self.vault.integrity_scan()

        assert results['scanned'] > 0
        assert results['verified'] == results['scanned']
        assert len(results['corrupted']) == 0
        assert len(results['errors']) == 0

    def test_stats(self):
        """Test vault stats."""
        # Store a file
        test_file = Path(self.tmpdir) / "test.bin"
        test_file.write_bytes(os.urandom(2000))
        self.vault.store_file(str(test_file))

        stats = self.vault.stats()

        assert stats['chunk_count'] > 0
        assert stats['total_size_bytes'] > 0
        assert stats['chunk_size'] == 1024


class TestSecurityFeatures:
    """Test security features."""

    def setup_method(self):
        """Set up temp directory."""
        self.tmpdir = tempfile.mkdtemp()
        self.vault_path = Path(self.tmpdir) / "vault"
        self.vault = ChunkVaultHardened(str(self.vault_path), chunk_size=1024)

    def teardown_method(self):
        """Clean up."""
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def test_path_traversal_blocked(self):
        """Test path traversal is blocked."""
        test_file = Path(self.tmpdir) / "test.bin"
        test_file.write_bytes(b"test")
        manifest = self.vault.store_file(str(test_file))

        with pytest.raises(ValueError, match="traversal"):
            self.vault.regenerate(manifest, "../../../etc/passwd")

    def test_invalid_chunk_hash_rejected(self):
        """Test invalid chunk hash is rejected."""
        with pytest.raises(ValueError, match="Invalid chunk hash"):
            self.vault._chunk_path("not_a_valid_hash")

    def test_manifest_validation_required(self):
        """Test invalid manifest is rejected during regenerate."""
        bad_manifest = FileManifest(
            name="bad",
            source_path="/bad",
            file_sha256="x" * 64,
            file_size=0,
            algo="unknown",
            chunk_size=0,
            chunks=[],
            created="",
        )

        with pytest.raises(ValueError, match="Invalid manifest"):
            self.vault.regenerate(bad_manifest, "output.bin")


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
