from datetime import datetime
from pathlib import Path

import pytest

from erk_shared.naming import (
    WORKTREE_DATE_SUFFIX_FORMAT,
    InvalidObjectiveSlug,
    InvalidPlanTitle,
    InvalidWorktreeName,
    ValidObjectiveSlug,
    ValidPlanTitle,
    ValidWorktreeName,
    default_branch_for_worktree,
    ensure_unique_worktree_name,
    extract_objective_number,
    generate_planned_pr_branch_name,
    sanitize_branch_component,
    sanitize_worktree_name,
    slugify_node_description,
    strip_plan_from_filename,
    validate_objective_slug,
    validate_plan_title,
    validate_worktree_name,
)


def _get_current_date_suffix() -> str:
    """Get the current date suffix for plan-derived worktrees."""
    return datetime.now().strftime(WORKTREE_DATE_SUFFIX_FORMAT)


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("Foo", "foo"),
        (" Foo Bar ", "foo-bar"),
        ("A/B C", "a/b-c"),
        ("@@weird!!name??", "weird-name"),
        # Test truncation to 31 characters
        ("a" * 35, "a" * 31),
        (
            "this-is-a-very-long-branch-name-that-exceeds-thirty-characters",
            "this-is-a-very-long-branch-name",
        ),
        ("exactly-31-characters-long-oka", "exactly-31-characters-long-oka"),
        (
            "32-characters-long-should-be-abc",
            "32-characters-long-should-be-ab",
        ),  # Truncates to 31
        ("short", "short"),
        # Test long names with trailing hyphens are stripped
        (
            "branch-name-with-dash-at-position-31-",
            "branch-name-with-dash-at-positi",
        ),
        # Test very long names truncate to 31
        (
            "1234567890123456789012345678901-extra",
            "1234567890123456789012345678901",
        ),  # Hyphen at position 31 stripped
        # Test dot handling - dots should be replaced with hyphens
        (".hidden-file", "hidden-file"),
        ("file.extension", "file-extension"),
    ],
)
def test_sanitize_branch_component(value: str, expected: str) -> None:
    assert sanitize_branch_component(value) == expected


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("feature X", "feature-x"),
        ("/ / ", "work"),
    ],
)
def test_default_branch_for_worktree(value: str, expected: str) -> None:
    assert default_branch_for_worktree(value) == expected


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("Foo", "foo"),
        ("Add_Auth_Feature", "add-auth-feature"),
        ("My_Cool_Plan", "my-cool-plan"),
        ("FOO_BAR_BAZ", "foo-bar-baz"),
        ("feature__with___multiple___underscores", "feature-with-multiple-underscor"),
        ("name-with-hyphens", "name-with-hyphens"),
        ("Mixed_Case-Hyphen_Underscore", "mixed-case-hyphen-underscore"),
        ("@@weird!!name??", "weird-name"),
        ("   spaces   ", "spaces"),
        ("---", "work"),
        # Test truncation to 31 characters
        ("a" * 35, "a" * 31),
        (
            "this-is-a-very-long-worktree-name-that-exceeds-thirty-characters",
            "this-is-a-very-long-worktree-na",
        ),
        ("exactly-31-characters-long-oka", "exactly-31-characters-long-oka"),
        (
            "32-characters-long-should-be-abc",
            "32-characters-long-should-be-ab",
        ),  # Truncates to 31
        # Test truncation with trailing hyphen removal
        (
            "worktree-name-with-dash-at-position-31-",
            "worktree-name-with-dash-at-posi",
        ),
        # Test truncation that ends with hyphen is stripped
        (
            "1234567890123456789012345678901-extra",
            "1234567890123456789012345678901",
        ),  # Hyphen at position 31 stripped
        # Test dot handling - dots should be replaced with hyphens
        (".worker-impl", "worker-impl"),
        ("fix-.worker", "fix-worker"),
        ("name.with.dots", "name-with-dots"),
    ],
)
def test_sanitize_worktree_name(value: str, expected: str) -> None:
    assert sanitize_worktree_name(value) == expected


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("devclikit-extraction-plan", "devclikit-extraction"),
        ("my-feature-plan", "my-feature"),
        ("plan-for-auth", "for-auth"),
        ("plan-something", "something"),
        ("something-plan", "something"),
        ("something-plan-else", "something-else"),
        ("plan-my-plan-feature", "my-feature"),
        ("my-plan-feature-plan", "my-feature"),
        ("plan", "plan"),
        ("my_feature_plan", "my_feature"),
        ("my feature plan", "my feature"),
        ("my-feature_plan", "my-feature"),
        ("MY-FEATURE-PLAN", "MY-FEATURE"),
        ("My-Feature-Plan", "My-Feature"),
        ("my-feature-PLAN", "my-feature"),
        ("airplane-feature", "airplane-feature"),
        ("explain-system", "explain-system"),
        ("planted-tree", "planted-tree"),
        ("planning-session", "planning-session"),
        ("plans-document", "plans-document"),
        ("-plan-feature", "feature"),
        ("feature-plan-", "feature"),
        ("my-feature-implementation-plan", "my-feature"),
        ("implementation-plan-for-auth", "for-auth"),
        ("implementation_plan_feature", "feature"),
        ("feature implementation plan", "feature"),
        ("my-feature_implementation-plan", "my-feature"),
        ("implementation_plan-for-auth", "for-auth"),
        ("IMPLEMENTATION-PLAN-FEATURE", "FEATURE"),
        ("Implementation-Plan-Feature", "Feature"),
        ("my-IMPLEMENTATION-plan", "my"),
        ("my-implementation-plan-feature", "my-feature"),
        ("plan-implementation-plan", "implementation"),
        ("plan implementation plan", "implementation"),
        ("implementation-plan", "implementation"),
        ("implementation_plan", "implementation"),
        ("IMPLEMENTATION-PLAN", "IMPLEMENTATION"),
        ("reimplementation-feature", "reimplementation-feature"),
        ("implantation-system", "implantation-system"),
    ],
)
def test_strip_plan_from_filename(value: str, expected: str) -> None:
    assert strip_plan_from_filename(value) == expected


def test_ensure_unique_worktree_name_first_time(tmp_path: Path) -> None:
    """Test first-time worktree creation gets only datetime suffix."""
    from erk_shared.gateway.git.real import RealGit

    repo_dir = tmp_path / "erks"
    repo_dir.mkdir()

    git_ops = RealGit()
    result = ensure_unique_worktree_name("my-feature", repo_dir, git_ops)

    # Should have datetime suffix in format -YY-MM-DD-HHMM
    date_suffix = _get_current_date_suffix()
    assert result == f"my-feature-{date_suffix}"
    assert not (repo_dir / result).exists()


def test_ensure_unique_worktree_name_duplicate_same_minute(tmp_path: Path) -> None:
    """Test duplicate worktree in same minute adds -2 after datetime suffix."""
    from erk_shared.gateway.git.real import RealGit

    repo_dir = tmp_path / "erks"
    repo_dir.mkdir()

    date_suffix = _get_current_date_suffix()
    existing_name = f"my-feature-{date_suffix}"
    (repo_dir / existing_name).mkdir()

    git_ops = RealGit()
    result = ensure_unique_worktree_name("my-feature", repo_dir, git_ops)

    assert result == f"my-feature-{date_suffix}-2"
    assert not (repo_dir / result).exists()
    assert (repo_dir / existing_name).exists()


def test_ensure_unique_worktree_name_multiple_duplicates(tmp_path: Path) -> None:
    """Test multiple duplicates increment correctly."""
    from erk_shared.gateway.git.real import RealGit

    repo_dir = tmp_path / "erks"
    repo_dir.mkdir()

    date_suffix = _get_current_date_suffix()
    (repo_dir / f"my-feature-{date_suffix}").mkdir()
    (repo_dir / f"my-feature-{date_suffix}-2").mkdir()
    (repo_dir / f"my-feature-{date_suffix}-3").mkdir()

    git_ops = RealGit()
    result = ensure_unique_worktree_name("my-feature", repo_dir, git_ops)

    assert result == f"my-feature-{date_suffix}-4"


def test_ensure_unique_worktree_name_with_existing_number(tmp_path: Path) -> None:
    """Test name with existing number in base preserves it."""
    from erk_shared.gateway.git.real import RealGit

    repo_dir = tmp_path / "erks"
    repo_dir.mkdir()

    git_ops = RealGit()
    date_suffix = _get_current_date_suffix()
    result = ensure_unique_worktree_name("fix-v3", repo_dir, git_ops)

    # Base name has number, should preserve it in datetime-suffixed name
    assert result == f"fix-v3-{date_suffix}"

    # Create it and try again
    (repo_dir / result).mkdir()
    result2 = ensure_unique_worktree_name("fix-v3", repo_dir, git_ops)

    assert result2 == f"fix-v3-{date_suffix}-2"


def test_sanitize_branch_component_truncates_at_31_chars() -> None:
    """Branch names should truncate to 31 characters maximum."""
    # Exactly 31 characters
    assert len(sanitize_branch_component("a" * 31)) == 31

    # 32 characters truncates to 31
    assert len(sanitize_branch_component("a" * 32)) == 31

    # Long descriptive name gets truncated
    long_name = "fix-dependency-injection-in-simplesubmitpy-to-eliminate-test-mocking"
    result = sanitize_branch_component(long_name)
    assert len(result) == 31
    assert not result.endswith("-")  # No trailing hyphens after truncation


def test_sanitize_branch_component_matches_worktree_length() -> None:
    """Branch and worktree names should have same length for same input."""
    test_name = "very-long-feature-name-that-exceeds-thirty-characters-easily"
    branch = sanitize_branch_component(test_name)
    worktree = sanitize_worktree_name(test_name)
    assert len(branch) == len(worktree)
    assert len(branch) == 31


def test_very_long_title_truncates_to_45_chars_total() -> None:
    """Regression test: 99-char title should truncate to max 45 chars total with datetime suffix.

    This tests the bug fix where `erk implement` created excessively long branch names.
    Example: "refactor erk implement command to support interactive and
    non-interactive execution modes"

    Note: The 31-char limit includes rstrip("-") after truncation,
    so actual length may be <= 31.
    """
    # 89-character title that caused the original bug
    long_title = (
        "refactor erk implement command to support interactive and non-interactive execution modes"
    )

    # Sanitize the worktree name (should be <= 31 chars max, trailing hyphens stripped)
    base_name = sanitize_worktree_name(long_title)
    assert len(base_name) <= 31

    # With datetime suffix (-YY-MM-DD-HHMM = 14 chars including hyphen), total should be <= 45 chars
    date_suffix = "25-11-23-1430"
    name_with_date = f"{base_name}-{date_suffix}"
    assert len(name_with_date) <= 45

    # Verify the base name is correctly truncated (30 chars after rstrip of trailing hyphen)
    assert base_name == "refactor-erk-implement-command"
    assert len(base_name) == 30  # 31 chars truncated, then trailing hyphen stripped


@pytest.mark.parametrize(
    ("branch_name", "expected"),
    [
        # Draft-PR branches with objective ID (current prefix)
        ("plnd/O456-fix-auth-bug-01-15-1430", 456),
        ("plnd/O1-add-tests-12-31-2359", 1),
        # Legacy planned/ prefix branches with objective ID
        ("planned/O456-fix-auth-bug-01-15-1430", 456),
        ("planned/O1-add-tests-12-31-2359", 1),
        # Legacy plan/ prefix branches with objective ID
        ("plan/O456-fix-auth-01-15-1430", 456),
        ("plan/O7709-plan-lazy-tip-sync-f-02-21-1116", 7709),
        # Without objective ID
        ("plnd/fix-auth-bug-01-15-1430", None),
        ("planned/fix-auth-bug-01-15-1430", None),
        ("plan/fix-auth-bug-01-15-1430", None),
        ("feature-branch", None),
        ("master", None),
    ],
)
def test_extract_objective_number(branch_name: str, expected: int | None) -> None:
    """Extract objective ID from branch name."""
    assert extract_objective_number(branch_name) == expected


# Tests for generate_planned_pr_branch_name
@pytest.mark.parametrize(
    ("title", "timestamp", "objective_id", "expected"),
    [
        # Standard case without objective
        (
            "Fix Auth Bug",
            datetime(2024, 1, 15, 14, 30),
            None,
            "plnd/fix-auth-bug-01-15-1430",
        ),
        # Different timestamp
        (
            "My Feature",
            datetime(2024, 6, 20, 10, 0),
            None,
            "plnd/my-feature-06-20-1000",
        ),
        # Midnight edge case
        (
            "Update Docs",
            datetime(2024, 1, 1, 0, 0),
            None,
            "plnd/update-docs-01-01-0000",
        ),
        # With objective ID
        (
            "Fix Auth Bug",
            datetime(2024, 1, 15, 14, 30),
            456,
            "plnd/O456-fix-auth-bug-01-15-1430",
        ),
        # Single digit objective
        (
            "Add Tests",
            datetime(2024, 12, 31, 23, 59),
            1,
            "plnd/O1-add-tests-12-31-2359",
        ),
    ],
)
def test_generate_planned_pr_branch_name_format(
    title: str, timestamp: datetime, objective_id: int | None, expected: str
) -> None:
    """Branch name follows plnd/{slug}-{timestamp} format."""
    assert generate_planned_pr_branch_name(title, timestamp, objective_id=objective_id) == expected


def test_generate_planned_pr_branch_name_truncates_long_title() -> None:
    """Long titles are truncated before timestamp is appended."""
    long_title = "This is a very long title that should be truncated before timestamp"
    timestamp = datetime(2024, 1, 15, 14, 30)

    result = generate_planned_pr_branch_name(long_title, timestamp, objective_id=None)

    # Base (plan-...) should be truncated to 31 chars, then timestamp appended
    # Total = 31 + 11 (timestamp with hyphen) = 42 chars max
    assert len(result) <= 42
    assert result.startswith("plnd/")
    assert result.endswith("-01-15-1430")
    # No trailing hyphen before timestamp
    assert not result[:-11].endswith("-")


def test_generate_planned_pr_branch_name_handles_special_chars() -> None:
    """Special characters in titles are sanitized."""
    result = generate_planned_pr_branch_name(
        "Fix: Bug #456!", datetime(2024, 1, 15, 14, 30), objective_id=None
    )
    assert ":" not in result
    assert "#" not in result
    assert "!" not in result
    assert "--" not in result


def test_generate_planned_pr_branch_name_with_objective_truncates() -> None:
    """Long titles are truncated with objective ID prefix included."""
    long_title = "This is a very long title that should be truncated before timestamp"
    timestamp = datetime(2024, 1, 15, 14, 30)

    result = generate_planned_pr_branch_name(long_title, timestamp, objective_id=456)

    assert len(result) <= 42
    assert result.startswith("plnd/O456-")
    assert result.endswith("-01-15-1430")
    assert not result[:-11].endswith("-")


# Tests for validate_objective_slug
@pytest.mark.parametrize(
    "slug",
    [
        "build-auth-system",
        "refactor-gateway",
        "add-dark-mode",
        "simple",
        "abc",
        "a" * 40,
        "fix-tui-layout",
        "add-v2-support",
        "feature123",
    ],
)
def test_validate_objective_slug_valid(slug: str) -> None:
    """Valid slugs return ValidObjectiveSlug."""
    result = validate_objective_slug(slug)
    assert isinstance(result, ValidObjectiveSlug)
    assert result.slug == slug


@pytest.mark.parametrize(
    ("slug", "reason_fragment"),
    [
        ("ab", "Too short"),
        ("a" * 41, "Too long"),
        ("Build-Auth", "Does not match"),
        ("UPPERCASE", "Does not match"),
        ("123-start", "Does not match"),
        ("my--slug", "Does not match"),
        ("has spaces", "Does not match"),
        ("with_underscores", "Does not match"),
        ("has.dots", "Does not match"),
        ("-leading-hyphen", "Does not match"),
        ("trailing-hyphen-", "Does not match"),
    ],
)
def test_validate_objective_slug_invalid(slug: str, reason_fragment: str) -> None:
    """Invalid slugs return InvalidObjectiveSlug with matching reason."""
    result = validate_objective_slug(slug)
    assert isinstance(result, InvalidObjectiveSlug)
    assert reason_fragment in result.reason


def test_validate_objective_slug_error_message_includes_pattern() -> None:
    """Error message includes the regex pattern for agent self-correction."""
    result = validate_objective_slug("INVALID")
    assert isinstance(result, InvalidObjectiveSlug)
    assert "^[a-z][a-z0-9]*(-[a-z0-9]+)*$" in result.message
    assert "INVALID" in result.message


# Tests for validate_plan_title
@pytest.mark.parametrize(
    "title",
    [
        "Add User Authentication",
        "Refactor Gateway Layer",
        "Fix CLI Argument Parsing",
        "hello",
        "a" * 100,
        "Fix Bug #123 in Auth Module",
        "café au lait feature",
        "Update the README file",
        "Add v2 API support for external clients",
    ],
)
def test_validate_plan_title_valid(title: str) -> None:
    """Valid titles return ValidPlanTitle."""
    result = validate_plan_title(title)
    assert isinstance(result, ValidPlanTitle)
    assert result.title == title.strip()


@pytest.mark.parametrize(
    ("title", "reason_fragment"),
    [
        ("", "empty"),
        ("   ", "empty"),
        ("\t\n", "empty"),
        ("Plan", "Too short"),
        ("ab", "Too short"),
        ("a" * 101, "Too long"),
        ("12345", "at least one alphabetic"),
        ("12345 67890", "at least one alphabetic"),
        ("!@#$%^&*()", "at least one alphabetic"),
        ("\U0001f680\U0001f389\U0001f525\U0001f4a5\U0001f31f", "at least one alphabetic"),
        ("\u4f60\u597d\u4e16\u754c\u7684\u8ba1\u5212", "No usable content"),
    ],
)
def test_validate_plan_title_invalid(title: str, reason_fragment: str) -> None:
    """Invalid titles return InvalidPlanTitle with matching reason."""
    result = validate_plan_title(title)
    assert isinstance(result, InvalidPlanTitle)
    assert reason_fragment in result.reason


def test_validate_plan_title_error_message_includes_rules() -> None:
    """Error message includes rules for agent self-correction."""
    result = validate_plan_title("")
    assert isinstance(result, InvalidPlanTitle)
    assert "5-100 characters" in result.message
    assert "at least one alphabetic" in result.message
    assert "Valid examples" in result.message
    assert "Invalid examples" in result.message


def test_validate_plan_title_error_type() -> None:
    """Error type is machine-readable."""
    result = validate_plan_title("")
    assert isinstance(result, InvalidPlanTitle)
    assert result.error_type == "invalid-plan-title"


def test_validate_plan_title_strips_whitespace() -> None:
    """Leading/trailing whitespace is stripped before validation."""
    result = validate_plan_title("  Add Feature  ")
    assert isinstance(result, ValidPlanTitle)
    assert result.title == "Add Feature"


def test_validate_plan_title_preserves_original_in_error() -> None:
    """Error includes the original unmodified title."""
    result = validate_plan_title("   ab   ")
    assert isinstance(result, InvalidPlanTitle)
    assert result.raw_title == "   ab   "


# Tests for validate_worktree_name
@pytest.mark.parametrize(
    "name",
    [
        "my-feature",
        "fix-auth-bug",
        "add-v2-support",
        "add-auth-feature",
        "fix-bug-123",
        "simple",
        "a",
        "work",
        "feature123",
        "123-fix-bug",
        "a" * 31,
        "my-feature-01-15-1430",
    ],
)
def test_validate_worktree_name_valid(name: str) -> None:
    """Valid worktree names return ValidWorktreeName."""
    result = validate_worktree_name(name)
    assert isinstance(result, ValidWorktreeName)
    assert result.name == name


def test_validate_worktree_name_valid_with_timestamp_suffix() -> None:
    """Names with timestamp suffixes are accepted as-is."""
    result = validate_worktree_name("42-feature-01-15-1430")
    assert isinstance(result, ValidWorktreeName)
    assert result.name == "42-feature-01-15-1430"


def test_validate_worktree_name_strips_whitespace() -> None:
    """Leading/trailing whitespace is stripped before validation."""
    result = validate_worktree_name("  my-feature  ")
    assert isinstance(result, ValidWorktreeName)
    assert result.name == "my-feature"


@pytest.mark.parametrize(
    ("name", "reason_fragment"),
    [
        ("My_Feature", "uppercase"),
        ("UPPERCASE", "uppercase"),
        ("camelCase", "uppercase"),
        ("has_underscores", "underscores"),
        ("my feature", "invalid characters"),
        ("name!with@special", "invalid characters"),
        ("name.with.dots", "invalid characters"),
        ("double--hyphen", "consecutive hyphens"),
        ("-leading-hyphen", "leading or trailing"),
        ("trailing-hyphen-", "leading or trailing"),
        ("a" * 32, "Too long"),
        ("a" * 50, "Too long"),
    ],
)
def test_validate_worktree_name_invalid(name: str, reason_fragment: str) -> None:
    """Invalid worktree names return InvalidWorktreeName with matching diagnostics."""
    result = validate_worktree_name(name)
    assert isinstance(result, InvalidWorktreeName)
    diag_text = " ".join(result.diagnostics)
    assert reason_fragment in diag_text, (
        f"Expected {reason_fragment!r} in diagnostics: {result.diagnostics}"
    )


def test_validate_worktree_name_empty() -> None:
    """Empty names return InvalidWorktreeName."""
    result = validate_worktree_name("")
    assert isinstance(result, InvalidWorktreeName)
    assert "Empty" in result.reason

    result_spaces = validate_worktree_name("   ")
    assert isinstance(result_spaces, InvalidWorktreeName)
    assert "Empty" in result_spaces.reason


def test_validate_worktree_name_error_type() -> None:
    """Error type is machine-readable."""
    result = validate_worktree_name("BAD_NAME")
    assert isinstance(result, InvalidWorktreeName)
    assert result.error_type == "invalid-worktree-name"


def test_validate_worktree_name_format_message_includes_rules() -> None:
    """format_message() contains rules, examples, and diagnostics."""
    result = validate_worktree_name("BAD_NAME")
    assert isinstance(result, InvalidWorktreeName)
    msg = result.format_message()
    assert "Lowercase letters, digits, and hyphens only" in msg
    assert "No underscores" in msg
    assert "No consecutive hyphens" in msg
    assert "Maximum 31 characters" in msg
    assert "Valid examples" in msg
    assert "Invalid examples" in msg
    assert "Diagnostics" in msg


def test_validate_worktree_name_preserves_original_in_error() -> None:
    """Error includes the original unmodified name."""
    result = validate_worktree_name("  BAD  ")
    assert isinstance(result, InvalidWorktreeName)
    assert result.raw_name == "  BAD  "


def test_validate_worktree_name_diagnostics_are_specific() -> None:
    """Diagnostics list is non-empty and identifies specific issues."""
    result = validate_worktree_name("BAD_NAME")
    assert isinstance(result, InvalidWorktreeName)
    assert len(result.diagnostics) > 0
    # Should detect both uppercase and underscore issues
    diag_text = " ".join(result.diagnostics)
    assert "uppercase" in diag_text.lower()
    assert "underscore" in diag_text.lower()


def test_validate_worktree_name_roundtrip_with_sanitize() -> None:
    """Valid names are exactly what sanitize_worktree_name produces."""
    valid_names = ["my-feature", "fix-bug", "add-v2-support", "work", "a" * 31]
    for name in valid_names:
        assert sanitize_worktree_name(name) == name, f"Valid name {name!r} changed by sanitize"


# ---------------------------------------------------------------------------
# slugify_node_description tests
# ---------------------------------------------------------------------------


def test_slugify_node_description_returns_hash_based_slug() -> None:
    """Hash-based slug generation produces node-<shorthash> format."""
    result = slugify_node_description("Add user model")
    assert result.startswith("node-")
    assert len(result) == 13  # "node-" + 8 hex chars


def test_slugify_node_description_deterministic() -> None:
    """Same description always produces the same slug."""
    assert slugify_node_description("Add user model") == slugify_node_description("Add user model")


def test_slugify_node_description_different_inputs() -> None:
    """Different descriptions produce different slugs."""
    a = slugify_node_description("Add user model")
    b = slugify_node_description("Wire into the CLI")
    assert a != b


def test_slugify_node_description_empty_string() -> None:
    """Empty description still produces a valid hash-based slug."""
    result = slugify_node_description("")
    assert result.startswith("node-")
    assert len(result) == 13
