"""Tests for enhanced lockfile - Issue #23"""

import pytest
from pathlib import Path
from argparse import Namespace

from nlsc.lockfile import (
    ANLULock,
    ModuleLock,
    Lockfile,
    hash_content,
    hash_anlu,
    generate_lockfile,
    write_lockfile,
    read_lockfile,
    verify_lockfile,
)
from nlsc.schema import NLFile, Module, ANLU, Input
from nlsc.parser import parse_nl_file


class TestBasicLockfile:
    """Tests for basic lockfile functionality"""

    def test_hash_content_deterministic(self):
        """Same content should produce same hash"""
        content = "def foo(): pass"
        h1 = hash_content(content)
        h2 = hash_content(content)
        assert h1 == h2
        assert h1.startswith("sha256:")

    def test_hash_content_different(self):
        """Different content should produce different hash"""
        h1 = hash_content("def foo(): pass")
        h2 = hash_content("def bar(): pass")
        assert h1 != h2

    def test_anlu_lock_dataclass(self):
        """ANLULock should store basic fields"""
        lock = ANLULock(
            source_hash="sha256:abc",
            output_hash="sha256:def",
            output_lines=10,
        )
        assert lock.source_hash == "sha256:abc"
        assert lock.output_hash == "sha256:def"
        assert lock.output_lines == 10


class TestEnhancedANLULock:
    """Tests for enhanced ANLU locking with inline code"""

    def test_anlu_lock_with_generated_code(self):
        """ANLULock should store generated code inline"""
        generated = '''def add(a: float, b: float) -> float:
    """Add two numbers"""
    return a + b
'''
        lock = ANLULock(
            source_hash="sha256:abc",
            output_hash=hash_content(generated),
            output_lines=4,
            generated_code=generated,
        )
        assert lock.generated_code == generated
        assert "def add" in lock.generated_code

    def test_generate_lockfile_includes_code(self):
        """generate_lockfile should include generated code per ANLU"""
        nl_content = """\
@module test
@target python

[add]
PURPOSE: Add two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a + b
"""
        nl_file = parse_nl_file(nl_content)
        generated_code = '''"""
Generated by nlsc
"""

def add(a: float, b: float) -> float:
    """Add two numbers."""
    return a + b
'''
        lockfile = generate_lockfile(nl_file, generated_code, "test.py")

        # Check that lockfile has the module
        assert "test" in lockfile.modules
        mod_lock = lockfile.modules["test"]

        # Check that ANLU has generated code
        assert "add" in mod_lock.anlus
        anlu_lock = mod_lock.anlus["add"]
        assert anlu_lock.generated_code is not None
        assert "def add" in anlu_lock.generated_code


class TestIncrementalCompilation:
    """Tests for detecting which ANLUs need recompilation"""

    def test_detect_unchanged_anlu(self):
        """Unchanged ANLU should not be marked for recompile"""
        nl_content = """\
@module test
@target python

[add]
PURPOSE: Add two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a + b
"""
        nl_file = parse_nl_file(nl_content)

        # Create lockfile
        generated = "def add(a, b): return a + b"
        lockfile = generate_lockfile(nl_file, generated, "test.py")

        # Verify same file - no changes
        errors = verify_lockfile(lockfile, nl_file)
        assert len(errors) == 0

    def test_detect_changed_anlu(self):
        """Changed ANLU should be detected"""
        nl_original = """\
@module test
@target python

[add]
PURPOSE: Add two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a + b
"""
        nl_modified = """\
@module test
@target python

[add]
PURPOSE: Add two numbers with new behavior.
INPUTS:
  - a: number
  - b: number
  - c: number
RETURNS: a + b + c
"""
        nl_file_orig = parse_nl_file(nl_original)
        nl_file_mod = parse_nl_file(nl_modified)

        # Create lockfile from original
        lockfile = generate_lockfile(nl_file_orig, "def add(a, b): pass", "test.py")

        # Verify modified file - should detect change
        errors = verify_lockfile(lockfile, nl_file_mod)
        assert len(errors) > 0
        assert any("changed" in e.lower() for e in errors)

    def test_detect_new_anlu(self):
        """New ANLU should be detected as missing from lockfile"""
        nl_original = """\
@module test
@target python

[add]
PURPOSE: Add two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a + b
"""
        nl_modified = """\
@module test
@target python

[add]
PURPOSE: Add two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a + b

[subtract]
PURPOSE: Subtract two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a - b
"""
        nl_file_orig = parse_nl_file(nl_original)
        nl_file_mod = parse_nl_file(nl_modified)

        # Create lockfile from original
        lockfile = generate_lockfile(nl_file_orig, "def add(a, b): pass", "test.py")

        # Verify modified file - should detect new ANLU
        errors = verify_lockfile(lockfile, nl_file_mod)
        assert len(errors) > 0
        assert any("subtract" in e.lower() for e in errors)


class TestLockfileReadWrite:
    """Tests for lockfile serialization with enhanced format"""

    def test_write_lockfile_includes_generated_code(self, tmp_path):
        """Written lockfile should include generated_code in YAML format"""
        generated = '''def add(a: float, b: float) -> float:
    """Add two numbers"""
    return a + b
'''
        lockfile = Lockfile()
        lockfile.modules["test"] = ModuleLock(
            source_hash="sha256:abc",
            anlus={
                "add": ANLULock(
                    source_hash="sha256:def",
                    output_hash="sha256:ghi",
                    output_lines=4,
                    generated_code=generated,
                )
            }
        )

        lock_path = tmp_path / "test.nl.lock"
        write_lockfile(lockfile, lock_path)

        content = lock_path.read_text()
        assert "generated_code:" in content
        assert "def add" in content

    def test_read_lockfile_recovers_generated_code(self, tmp_path):
        """Read lockfile should recover generated_code"""
        generated = '''def add(a: float, b: float) -> float:
    """Add two numbers"""
    return a + b
'''
        lockfile = Lockfile()
        lockfile.modules["test"] = ModuleLock(
            source_hash="sha256:abc",
            anlus={
                "add": ANLULock(
                    source_hash="sha256:def",
                    output_hash="sha256:ghi",
                    output_lines=4,
                    generated_code=generated,
                )
            }
        )

        lock_path = tmp_path / "test.nl.lock"
        write_lockfile(lockfile, lock_path)

        # Read back
        recovered = read_lockfile(lock_path)
        assert recovered is not None

        # Check structure was recovered
        assert "test" in recovered.modules
        assert "add" in recovered.modules["test"].anlus
        anlu_lock = recovered.modules["test"].anlus["add"]
        assert anlu_lock.generated_code == generated


class TestZeroLLMRebuild:
    """Tests for zero-LLM rebuild from lockfile"""

    def test_rebuild_from_lockfile_no_changes(self, tmp_path):
        """Rebuild unchanged file should use cached code from lockfile"""
        from nlsc.lockfile import rebuild_from_lockfile

        nl_content = """\
@module test
@target python

[add]
PURPOSE: Add two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a + b
"""
        nl_file = parse_nl_file(nl_content)

        cached_code = '''def add(a: float, b: float) -> float:
    """Add two numbers."""
    return a + b
'''

        # Create lockfile with cached code
        lockfile = Lockfile()
        lockfile.modules["test"] = ModuleLock(
            source_hash=hash_content(nl_content),
            anlus={
                "add": ANLULock(
                    source_hash=hash_anlu(nl_file.anlus[0]),
                    output_hash=hash_content(cached_code),
                    output_lines=4,
                    generated_code=cached_code,
                )
            }
        )

        # Rebuild should return cached code, not call LLM
        result = rebuild_from_lockfile(nl_file, lockfile)

        assert result is not None
        assert result.used_cache is True
        assert result.llm_calls == 0
        assert "def add" in result.code


class TestLockCommands:
    """Tests for nlsc lock:check and lock:update commands"""

    def test_cmd_lock_check_exists(self):
        """cmd_lock_check function should exist"""
        from nlsc.cli import cmd_lock_check
        assert callable(cmd_lock_check)

    def test_cmd_lock_update_exists(self):
        """cmd_lock_update function should exist"""
        from nlsc.cli import cmd_lock_update
        assert callable(cmd_lock_update)

    def test_lock_check_valid(self, tmp_path):
        """lock:check should return 0 for valid lockfile"""
        from nlsc.cli import cmd_lock_check

        nl_content = """\
@module test
@target python

[add]
PURPOSE: Add two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a + b
"""
        nl_file = tmp_path / "test.nl"
        nl_file.write_text(nl_content)

        # Generate lockfile
        nl_parsed = parse_nl_file(nl_content)
        lockfile = generate_lockfile(nl_parsed, "def add(): pass", "test.py")
        lock_path = tmp_path / "test.nl.lock"
        write_lockfile(lockfile, lock_path)

        args = Namespace(file=str(nl_file))
        result = cmd_lock_check(args)
        assert result == 0

    def test_lock_check_invalid(self, tmp_path):
        """lock:check should return 1 for outdated lockfile"""
        from nlsc.cli import cmd_lock_check

        nl_original = """\
@module test
@target python

[add]
PURPOSE: Add two numbers.
INPUTS:
  - a: number
  - b: number
RETURNS: a + b
"""
        nl_modified = """\
@module test
@target python

[add]
PURPOSE: Add three numbers.
INPUTS:
  - a: number
  - b: number
  - c: number
RETURNS: a + b + c
"""
        nl_file = tmp_path / "test.nl"
        nl_file.write_text(nl_original)

        # Generate lockfile from original
        nl_parsed = parse_nl_file(nl_original)
        lockfile = generate_lockfile(nl_parsed, "def add(): pass", "test.py")
        lock_path = tmp_path / "test.nl.lock"
        write_lockfile(lockfile, lock_path)

        # Modify the source
        nl_file.write_text(nl_modified)

        args = Namespace(file=str(nl_file))
        result = cmd_lock_check(args)
        assert result == 1
