# tests/test_integration_cli.py
import pytest
import json
import subprocess
import sys
import os
import pyperclip
from pathlib import Path
from unittest.mock import patch, MagicMock


@pytest.fixture
def sample_project(tmp_path):
    """Create a sample project structure for testing."""
    project_dir = tmp_path / "test_project"
    project_dir.mkdir()

    # Create various file types
    (project_dir / "main.py").write_text('print("Hello World")')
    (project_dir / "config.json").write_text('{"key": "value"}')
    (project_dir / "README.md").write_text("# Test Project\n\nThis is a test.")

    # Create subdirectories
    src_dir = project_dir / "src"
    src_dir.mkdir()
    (src_dir / "utils.py").write_text("def helper(): pass")
    (src_dir / "module.js").write_text("console.log('test');")

    tests_dir = project_dir / "tests"
    tests_dir.mkdir()
    (tests_dir / "test_main.py").write_text("def test_example(): assert True")

    # Create files to exclude
    node_modules = project_dir / "node_modules"
    node_modules.mkdir()
    (node_modules / "package.js").write_text("// node module")

    dist_dir = project_dir / "dist"
    dist_dir.mkdir()
    (dist_dir / "bundle.js").write_text("// compiled bundle")

    # Create log files
    (project_dir / "app.log").write_text("log entry")
    (project_dir / "error.log").write_text("error entry")

    # Create .gitignore
    gitignore_content = """
node_modules/
dist/
*.log
*.tmp
"""
    (project_dir / ".gitignore").write_text(gitignore_content.strip())

    return project_dir


@pytest.fixture
def aicodec_config(sample_project):
    """Create a basic aicodec configuration."""
    config_dir = sample_project / ".aicodec"
    config_dir.mkdir()

    config_data = {
        "aggregate": {
            "directory": ".",
            "use_gitignore": True,
            "exclude_dirs": [".git", ".aicodec"]
        },
        "prompt": {
            "output_file": ".aicodec/prompt.txt",
            "template": "Test template: {user_task_description}\n{code_context_json}"
        },
        "prepare": {
            "changes": ".aicodec/changes.json",
            "from_clipboard": False
        },
        "apply": {
            "output_dir": "."
        }
    }

    config_file = config_dir / "config.json"
    config_file.write_text(json.dumps(config_data, indent=2))

    return config_file


@pytest.fixture
def sample_changes_file(tmp_path):
    """Create a sample changes file for testing."""
    changes_data = {
        "summary": "Test changes for integration testing",
        "changes": [
            {
                "filePath": "new_file.py",
                "action": "CREATE",
                "content": "# New file\nprint('Hello from new file')"
            },
            {
                "filePath": "main.py",
                "action": "REPLACE",
                "content": "# Modified main file\nprint('Hello Modified World')"
            },
            {
                "filePath": "old_file.py",
                "action": "DELETE",
                "content": ""
            }
        ]
    }

    changes_file = tmp_path / "changes.json"
    changes_file.write_text(json.dumps(changes_data, indent=2))
    return changes_file


def run_aicodec_command(args, cwd=None, input_text=None, env_extra=None):
    """Helper function to run aicodec commands."""
    cmd = [sys.executable, "-m",
           "aicodec.infrastructure.cli.command_line_interface"] + args

    try:
        env = os.environ.copy()
        env['AICODEC_NO_EDITOR'] = '1'
        env['AICODEC_TEST_MODE'] = '1'
        if env_extra:
            env.update(env_extra)
        result = subprocess.run(
            cmd,
            cwd=cwd,
            capture_output=True,
            text=True,
            input=input_text,
            timeout=30,
            env=env
        )
        return result
    except subprocess.TimeoutExpired:
        pytest.fail("Command timed out")


# INIT COMMAND TESTS

def test_init_command_basic(tmp_path, monkeypatch):
    """Test basic init command with default options."""
    monkeypatch.chdir(tmp_path)

    inputs = 'y\ny\nn\nn\n'
    with patch('aicodec.infrastructure.cli.commands.utils.load_default_prompt_template', return_value="template"):
        result = run_aicodec_command(["init"], cwd=tmp_path, input_text=inputs)

    assert result.returncode == 0
    assert "Successfully created configuration" in result.stdout

    config_file = tmp_path / ".aicodec" / "config.json"
    assert config_file.exists()

    config = json.loads(config_file.read_text())
    assert config["aggregate"]["use_gitignore"] is True
    assert ".gitignore" in config["aggregate"]["exclude_files"]


def test_init_command_overwrite_existing(tmp_path, monkeypatch):
    """Test init command with existing config file."""
    monkeypatch.chdir(tmp_path)

    # Create existing config
    config_dir = tmp_path / ".aicodec"
    config_dir.mkdir()
    existing_config = config_dir / "config.json"
    existing_config.write_text('{"existing": "config"}')

    # Test cancellation
    result = run_aicodec_command(["init"], cwd=tmp_path, input_text='n\n')

    assert result.returncode == 0
    assert "Initialization cancelled" in result.stdout

    # Verify original config is unchanged
    config = json.loads(existing_config.read_text())
    assert config == {"existing": "config"}


def test_init_command_with_additional_options(tmp_path, monkeypatch):
    """Test init command with additional inclusions/exclusions."""
    monkeypatch.chdir(tmp_path)

    inputs = 'y\ny\ny\nsrc,lib\n*.py,*.js\n.py,.js,.json\nbuild,temp\n*.tmp,*.bak\n.log,.tmp\ny\n'
    with patch('aicodec.infrastructure.cli.commands.utils.load_default_prompt_template', return_value="template"):
        result = run_aicodec_command(["init"], cwd=tmp_path, input_text=inputs)

    assert result.returncode == 0

    config_file = tmp_path / ".aicodec" / "config.json"
    config = json.loads(config_file.read_text())

    assert "src" in config["aggregate"]["include_dirs"]
    assert "lib" in config["aggregate"]["include_dirs"]
    assert "*.py" in config["aggregate"]["include_files"]
    assert ".py" in config["aggregate"]["include_ext"]
    assert "build" in config["aggregate"]["exclude_dirs"]
    assert "*.tmp" in config["aggregate"]["exclude_files"]
    assert ".log" in config["aggregate"]["exclude_exts"]
    assert config["prepare"]["from_clipboard"] is True


# SCHEMA COMMAND TESTS

def test_schema_command(tmp_path):
    """Test schema command output."""
    result = run_aicodec_command(["schema"], cwd=tmp_path)

    assert result.returncode == 0
    json.loads(result.stdout)


# AGGREGATE COMMAND TESTS

def test_aggregate_command_basic(sample_project, aicodec_config, monkeypatch):
    """Test basic aggregate command."""
    monkeypatch.chdir(sample_project)

    result = run_aicodec_command(["aggregate", "-c", str(aicodec_config)])

    assert result.returncode == 0
    assert "Successfully aggregated" in result.stdout

    context_file = sample_project / ".aicodec" / "context.json"
    assert context_file.exists()

    context_data = json.loads(context_file.read_text())
    assert isinstance(context_data, list)
    assert len(context_data) > 0

    # Verify expected files are included
    file_paths = {item["filePath"] for item in context_data}
    assert "main.py" in file_paths
    assert "src/utils.py" in file_paths

    # Verify excluded files are not included (due to .gitignore)
    assert "app.log" not in file_paths
    assert "node_modules/package.js" not in file_paths


def test_aggregate_command_with_directory_override(sample_project, aicodec_config, monkeypatch):
    """Test aggregate command with directory override."""
    monkeypatch.chdir(sample_project.parent)

    result = run_aicodec_command([
        "aggregate", "-c", str(aicodec_config),
        "-d", str(sample_project)
    ])

    assert result.returncode == 0
    context_file = sample_project / ".aicodec" / "context.json"
    assert context_file.exists()


def test_aggregate_command_with_inclusions(sample_project, aicodec_config, monkeypatch):
    """Test aggregate command with inclusion overrides."""
    monkeypatch.chdir(sample_project)

    result = run_aicodec_command([
        "aggregate", "-c", str(aicodec_config),
        "--include-dir", "node_modules",
        "--include-ext", ".log",
        "--include-file", "dist/bundle.js"
    ])

    assert result.returncode == 0

    context_file = sample_project / ".aicodec" / "context.json"
    context_data = json.loads(context_file.read_text())
    file_paths = {item["filePath"] for item in context_data}

    # These should be included despite .gitignore
    assert "node_modules/package.js" in file_paths
    assert "app.log" in file_paths
    assert "dist/bundle.js" in file_paths


def test_aggregate_command_with_exclusions(sample_project, aicodec_config, monkeypatch):
    """Test aggregate command with exclusion overrides."""
    monkeypatch.chdir(sample_project)

    result = run_aicodec_command([
        "aggregate", "-c", str(aicodec_config),
        "--exclude-dir", "src",
        "--exclude-ext", ".py",
        "--exclude-file", "README.md"
    ])

    assert result.returncode == 0

    context_file = sample_project / ".aicodec" / "context.json"
    context_data = json.loads(context_file.read_text())
    file_paths = {item["filePath"] for item in context_data}

    # These should be excluded
    assert "src/utils.py" not in file_paths
    assert "main.py" not in file_paths
    assert "README.md" not in file_paths


def test_aggregate_command_full_run(sample_project, aicodec_config, monkeypatch):
    """Test aggregate command with full run option."""
    monkeypatch.chdir(sample_project)

    # First run to create hashes
    result1 = run_aicodec_command(["aggregate", "-c", str(aicodec_config)])
    assert result1.returncode == 0

    # Second run should detect no changes
    result2 = run_aicodec_command(["aggregate", "-c", str(aicodec_config)])
    assert result2.returncode == 0
    assert "No changes detected" in result2.stdout

    # Full run should aggregate all files regardless of hashes
    result3 = run_aicodec_command(
        ["aggregate", "-c", str(aicodec_config), "--full"])
    assert result3.returncode == 0
    assert "Successfully aggregated" in result3.stdout


def test_aggregate_command_count_tokens(sample_project, aicodec_config, monkeypatch):
    """Test aggregate command with token counting."""
    monkeypatch.chdir(sample_project)

    result = run_aicodec_command([
        "aggregate", "-c", str(aicodec_config), "--count-tokens"
    ])

    assert result.returncode == 0
    assert "Token count:" in result.stdout or "Token counting failed" in result.stdout


def test_aggregate_command_gitignore_options(sample_project, aicodec_config, monkeypatch):
    """Test aggregate command with gitignore options."""
    monkeypatch.chdir(sample_project)

    # Test with --no-gitignore
    result1 = run_aicodec_command([
        "aggregate", "-c", str(aicodec_config), "--no-gitignore"
    ])

    assert result1.returncode == 0
    context_file = sample_project / ".aicodec" / "context.json"
    context_data = json.loads(context_file.read_text())
    file_paths = {item["filePath"] for item in context_data}

    # With --no-gitignore, log files should be included
    assert "app.log" in file_paths

    # Test with explicit --use-gitignore
    result2 = run_aicodec_command([
        "aggregate", "-c", str(aicodec_config), "--use-gitignore"
    ])

    assert result2.returncode == 0


# PROMPT COMMAND TESTS

def test_prompt_command_basic(sample_project, aicodec_config, monkeypatch):
    """Test basic prompt command."""
    monkeypatch.chdir(sample_project)

    # First create context
    run_aicodec_command(["aggregate", "-c", str(aicodec_config)])

    result = run_aicodec_command([
        "prompt", "-c", str(aicodec_config),
        "--task", "Fix all bugs"
    ])

    assert result.returncode == 0
    assert "Successfully generated prompt" in result.stdout

    prompt_file = sample_project / ".aicodec" / "prompt.txt"
    assert prompt_file.exists()

    prompt_content = prompt_file.read_text()
    assert "Fix all bugs" in prompt_content
    assert "Test template:" in prompt_content


def test_prompt_command_to_clipboard(sample_project, aicodec_config, monkeypatch):
    """Test prompt command with clipboard output."""
    monkeypatch.chdir(sample_project)

    # First create context
    run_aicodec_command(["aggregate", "-c", str(aicodec_config)])

    result = run_aicodec_command([
        "prompt", "-c", str(aicodec_config),
        "--task", "Refactor code",
        "--clipboard"
    ])

    assert result.returncode == 0
    assert "successfully copied to test clipboard" in result.stdout


def test_prompt_command_custom_output_file(sample_project, aicodec_config, monkeypatch):
    """Test prompt command with custom output file."""
    monkeypatch.chdir(sample_project)

    # First create context
    run_aicodec_command(["aggregate", "-c", str(aicodec_config)])

    custom_output = sample_project / "custom_prompt.txt"

    result = run_aicodec_command([
        "prompt", "-c", str(aicodec_config),
        "--output-file", str(custom_output)
    ])

    assert result.returncode == 0
    assert custom_output.exists()


def test_prompt_command_missing_context(sample_project, aicodec_config, monkeypatch):
    """Test prompt command when context file is missing."""
    monkeypatch.chdir(sample_project)

    result = run_aicodec_command(["prompt", "-c", str(aicodec_config)])

    assert result.returncode == 1
    assert "Context file" in (
        result.stdout + result.stderr) and "not found" in (result.stdout + result.stderr)


# PREPARE COMMAND TESTS

def test_prepare_command_editor_mode(sample_project, aicodec_config, monkeypatch):
    """Test prepare command in editor mode."""
    monkeypatch.chdir(sample_project)

    result = run_aicodec_command(["prepare", "-c", str(aicodec_config)])

    assert result.returncode == 0
    assert "Opening in default editor" in result.stdout

    changes_file = sample_project / ".aicodec" / "changes.json"
    assert changes_file.exists()


def test_prepare_command_clipboard_mode(sample_project, aicodec_config, monkeypatch):
    """Test prepare command with clipboard input."""
    monkeypatch.chdir(sample_project)

    valid_json = json.dumps({
        "summary": "Test summary",
        "changes": [{
            "filePath": "test.py",
            "action": "CREATE",
            "content": "print('test')"
        }]
    })

    result = run_aicodec_command([
        "prepare", "-c", str(aicodec_config), "--from-clipboard"
    ], env_extra={'AICODEC_TEST_CLIPBOARD': valid_json})

    assert result.returncode == 0
    assert "Successfully wrote content from clipboard" in result.stdout

    changes_file = sample_project / ".aicodec" / "changes.json"
    assert changes_file.exists()
    assert json.loads(changes_file.read_text()) == json.loads(valid_json)


def test_prepare_command_clipboard_invalid_json(sample_project, aicodec_config, monkeypatch):
    """Test prepare command with invalid JSON in clipboard."""
    monkeypatch.chdir(sample_project)

    result = run_aicodec_command([
        "prepare", "-c", str(aicodec_config), "--from-clipboard"
    ], env_extra={'AICODEC_TEST_CLIPBOARD': 'invalid json'})

    assert result.returncode == 0
    assert " Clipboard content does not match the expected schema" in result.stdout


def test_prepare_command_overwrite_existing(sample_project, aicodec_config, monkeypatch):
    """Test prepare command with existing changes file."""
    monkeypatch.chdir(sample_project)

    # Create existing changes file
    changes_file = sample_project / ".aicodec" / "changes.json"
    changes_file.parent.mkdir(exist_ok=True)
    changes_file.write_text('{"existing": "data"}')

    # Test cancellation
    result = run_aicodec_command(
        ["prepare", "-c", str(aicodec_config)], input_text='n\n')

    assert result.returncode == 0
    assert "Operation cancelled" in result.stdout

    # Verify original content is unchanged
    assert json.loads(changes_file.read_text()) == {"existing": "data"}


def test_prepare_command_custom_changes_file(sample_project, aicodec_config, monkeypatch):
    """Test prepare command with custom changes file path."""
    monkeypatch.chdir(sample_project)

    custom_changes = sample_project / "my_changes.json"

    result = run_aicodec_command([
        "prepare", "-c", str(aicodec_config),
        "--changes", str(custom_changes)
    ])

    assert result.returncode == 0
    assert custom_changes.exists()


# APPLY AND REVERT COMMAND TESTS

def test_apply_command_basic(sample_project, aicodec_config, sample_changes_file, monkeypatch):
    """Test basic apply command (mocked server launch)."""
    monkeypatch.chdir(sample_project)

    # Update config to point to our sample changes file
    config = json.loads(aicodec_config.read_text())
    config["prepare"]["changes"] = str(sample_changes_file)
    aicodec_config.write_text(json.dumps(config, indent=2))

    result = run_aicodec_command([
        "apply", "-c", str(aicodec_config)
    ])

    assert result.returncode == 0
    assert "Test mode: skipping server launch" in result.stdout


def test_apply_command_with_overrides(sample_project, sample_changes_file, monkeypatch):
    """Test apply command with CLI parameter overrides."""
    monkeypatch.chdir(sample_project)

    # Create minimal config without apply section
    config_dir = sample_project / ".aicodec"
    config_dir.mkdir(exist_ok=True)
    config_file = config_dir / "config.json"
    config_file.write_text('{}')

    result = run_aicodec_command([
        "apply", "-c", str(config_file),
        "--output-dir", str(sample_project),
        "--changes", str(sample_changes_file)
    ])

    assert result.returncode == 0
    assert "Test mode: skipping server launch" in result.stdout


def test_apply_command_missing_config(sample_project, monkeypatch):
    """Test apply command with missing required configuration."""
    monkeypatch.chdir(sample_project)

    # Create config without required fields
    config_dir = sample_project / ".aicodec"
    config_dir.mkdir(exist_ok=True)
    config_file = config_dir / "config.json"
    config_file.write_text('{}')

    result = run_aicodec_command(["apply", "-c", str(config_file)])

    assert result.returncode == 0
    assert "Missing required configuration" in result.stdout


def test_revert_command_basic(sample_project, aicodec_config, monkeypatch):
    """Test basic revert command."""
    monkeypatch.chdir(sample_project)

    # Create mock revert file
    revert_dir = sample_project / ".aicodec"
    revert_dir.mkdir(exist_ok=True)
    revert_file = revert_dir / "revert.json"
    revert_data = {
        "changes": [{
            "filePath": "test.py",
            "action": "DELETE",
            "content": ""
        }]
    }
    revert_file.write_text(json.dumps(revert_data))

    result = run_aicodec_command([
        "revert", "-c", str(aicodec_config)
    ])

    assert result.returncode == 0
    assert "Test mode: skipping server launch" in result.stdout


def test_revert_command_no_revert_data(sample_project, aicodec_config, monkeypatch):
    """Test revert command when no revert data exists."""
    monkeypatch.chdir(sample_project)

    result = run_aicodec_command([
        "revert", "-c", str(aicodec_config)
    ])

    assert result.returncode == 0
    assert "No revert data found" in result.stdout


def test_revert_command_with_output_dir_override(sample_project, monkeypatch):
    """Test revert command with output directory override."""
    monkeypatch.chdir(sample_project)

    # Create minimal config
    config_dir = sample_project / ".aicodec"
    config_dir.mkdir(exist_ok=True)
    config_file = config_dir / "config.json"
    config_file.write_text('{}')

    # Create revert file
    revert_file = sample_project / ".aicodec" / "revert.json"
    revert_file.write_text('{"changes": []}')

    result = run_aicodec_command([
        "revert", "-c", str(config_file),
        "--output-dir", str(sample_project)
    ])

    assert result.returncode == 0
    assert "Test mode: skipping server launch" in result.stdout


# ERROR HANDLING AND EDGE CASES

def test_missing_config_file(tmp_path, monkeypatch):
    """Test commands that require config file when it doesn't exist."""
    monkeypatch.chdir(tmp_path)

    result = run_aicodec_command(["aggregate"])

    assert result.returncode == 1
    assert "aicodec not initialised" in result.stdout


def test_aggregate_no_files_to_aggregate(sample_project, monkeypatch):
    """Test aggregate command when no files match criteria."""
    monkeypatch.chdir(sample_project)

    # Create config that excludes everything
    config_dir = sample_project / ".aicodec"
    config_dir.mkdir(exist_ok=True)
    config_data = {
        "aggregate": {
            "directory": ".",
            "use_gitignore": True,
            "exclude_dirs": ["src", "tests"],
            "exclude_exts": [".py", ".js", ".json", ".md", ".log"],
            "exclude_files": ["*"]
        }
    }
    config_file = config_dir / "config.json"
    config_file.write_text(json.dumps(config_data))

    result = run_aicodec_command(["aggregate", "-c", str(config_file)])

    assert result.returncode == 0
    assert "No files found to aggregate" in result.stdout


def test_command_with_nonexistent_directory(tmp_path):
    """Test aggregate command with non-existent directory."""
    config_dir = tmp_path / ".aicodec"
    config_dir.mkdir()
    config_data = {
        "aggregate": {
            "directory": ".",
            "use_gitignore": True
        }
    }
    config_file = config_dir / "config.json"
    config_file.write_text(json.dumps(config_data))

    result = run_aicodec_command([
        "aggregate", "-c", str(config_file),
        "-d", "/nonexistent/path"
    ])

    # The command should handle this gracefully
    assert result.returncode == 0
    assert "No files found to aggregate" in result.stdout
