import os
import shutil
import subprocess
import textwrap
from pathlib import Path
from tempfile import mkdtemp
from unittest import TestCase

from abstra_internals.consts.filepaths import GITIGNORE_FILEPATH
from abstra_internals.services.fs import FileSystemService
from abstra_internals.settings import Settings


class TestFileSystemService(TestCase):
    def setUp(self):
        # Save original working directory to restore later
        self.original_cwd = Path.cwd()
        self.test_dir = Path(mkdtemp()) / "test_rm_tree_dir"
        Settings.set_root_path(str(self.test_dir.absolute()))
        self.test_dir.mkdir(exist_ok=True)
        (self.test_dir / "subdir").mkdir(exist_ok=True)
        (self.test_dir / "subdir" / "file.txt").write_text("hello")
        (self.test_dir / "file2.txt").write_text("world")
        (self.test_dir / "symlink").symlink_to(self.test_dir / "subdir")

    def tearDown(self):
        # Restore original working directory before cleaning up
        os.chdir(self.original_cwd)
        if self.test_dir.exists():
            shutil.rmtree(self.test_dir, ignore_errors=True)
        if Path("test_ignore_dir").exists():
            shutil.rmtree("test_ignore_dir", ignore_errors=True)

    def test_rm_tree_removes_all(self):
        FileSystemService.rm_tree(self.test_dir)
        self.assertFalse(self.test_dir.exists())

    def test_list_files_and_paths(self):
        test_dir = Path("test_ignore_dir")
        test_dir.mkdir(exist_ok=True)
        (test_dir / "file.txt").write_text("abc")
        (test_dir / "__pycache__").mkdir(exist_ok=True)
        (test_dir / "__pycache__" / "cache.pyc").write_text("cache")
        ignore_path = test_dir / GITIGNORE_FILEPATH
        ignore_path.write_text(
            "__pycache__/"
        )  # Fixed: use correct gitignore pattern for any __pycache__ directory
        files = FileSystemService.list_files(test_dir)
        file_names = [f.name for f in files]
        self.assertIn("file.txt", file_names)
        self.assertNotIn("__pycache__", file_names)
        paths = FileSystemService.list_paths(test_dir, include_dirs=True)
        dir_names = [p.name for p in paths]
        self.assertIn("file.txt", dir_names)
        self.assertIn("test_ignore_dir", dir_names)

    def test_suffix_allowed(self):
        p = Path("foo.py")
        self.assertTrue(FileSystemService._suffix_allowed(p, [".py"]))
        self.assertFalse(FileSystemService._suffix_allowed(p, [".txt"]))

    def test_is_ignored(self):
        self.test_dir.joinpath(GITIGNORE_FILEPATH).write_text("*.pyc\n__pycache__/\n")
        self.assertTrue(FileSystemService.is_ignored(Path("foo.pyc")))
        self.assertFalse(FileSystemService.is_ignored(Path("foo.py")))


class TestGitIgnoreCompatibility(TestCase):
    """Test suite to ensure FileSystemService.is_ignored follows gitignore rules correctly."""

    def setUp(self):
        # Save original working directory to restore later
        self.original_cwd = Path.cwd()
        self.test_dir = Path(mkdtemp()) / "gitignore_test"
        Settings.set_root_path(str(self.test_dir.absolute()))
        self.test_dir.mkdir(exist_ok=True)

        # Initialize git repository for testing
        subprocess.run(["git", "init"], cwd=self.test_dir, capture_output=True)
        subprocess.run(
            ["git", "config", "user.email", "test@test.com"],
            cwd=self.test_dir,
            capture_output=True,
        )
        subprocess.run(
            ["git", "config", "user.name", "Test User"],
            cwd=self.test_dir,
            capture_output=True,
        )

        # Create comprehensive test directory structure
        test_structure = [
            "README.md",
            "file.pyc",
            "main.py",
            "temp.tmp",
            ".env",
            "build/output.o",
            "build/debug/info.log",
            "src/main.py",
            "src/main.pyc",
            "src/utils.py",
            "docs/readme.txt",
            "docs/images/logo.png",
            "__pycache__/cache.pyc",
            "sub/__pycache__/cache2.pyc",
            "sub/dir/file.txt",
            "sub/dir/temp.tmp",
            "node_modules/package/index.js",
            "node_modules/other/lib.js",
            "logs/error.log",
            "logs/debug/trace.log",
            "dist/bundle.js",
            "dist/css/style.css",
        ]

        for path_str in test_structure:
            path = self.test_dir / path_str
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text("content")

    def tearDown(self):
        # Restore original working directory before cleaning up
        os.chdir(self.original_cwd)
        if self.test_dir.exists():
            shutil.rmtree(self.test_dir, ignore_errors=True)

    def _create_ignore_file(self, content: str):
        """Helper to create ignore file with given content."""
        ignore_file = self.test_dir / GITIGNORE_FILEPATH
        # Dedent to remove leading spaces from triple-quoted strings
        # This ensures .gitignore has proper formatting for git check-ignore
        normalized_content = textwrap.dedent(content)
        ignore_file.write_text(normalized_content)
        # Clear git repository cache to ensure it picks up the new .gitignore
        FileSystemService._git_repository = None
        FileSystemService.clear_gitignore_cache()

    def _assert_ignored(self, pattern: str, path: str, should_be_ignored: bool):
        """Helper to test if a path is ignored by a pattern."""
        self._create_ignore_file(pattern)
        full_path = self.test_dir / path
        is_ignored = FileSystemService.is_ignored(full_path)
        self.assertEqual(
            is_ignored,
            should_be_ignored,
            f"Pattern '{pattern}' on path '{path}': expected {should_be_ignored}, got {is_ignored}",
        )

    def test_basic_wildcard_patterns(self):
        """Test basic wildcard patterns like *.ext"""
        # *.pyc should match all .pyc files at any level
        self._assert_ignored("*.pyc", "file.pyc", True)
        self._assert_ignored("*.pyc", "src/main.pyc", True)
        self._assert_ignored("*.pyc", "main.py", False)

        # *.tmp should match all .tmp files
        self._assert_ignored("*.tmp", "temp.tmp", True)
        self._assert_ignored("*.tmp", "sub/dir/temp.tmp", True)
        self._assert_ignored("*.tmp", "main.py", False)

    def test_directory_patterns_with_trailing_slash(self):
        """Test patterns ending with / that only match directories"""
        # __pycache__/ should match any __pycache__ directory
        self._assert_ignored("__pycache__/", "__pycache__", True)
        self._assert_ignored("__pycache__/", "sub/__pycache__", True)

        # node_modules/ should match node_modules directory
        self._assert_ignored("node_modules/", "node_modules", True)

        # build/ should match build directory but not build.txt file (if it existed)
        self._assert_ignored("build/", "build", True)

    def test_leading_slash_patterns(self):
        """Test patterns starting with / that are relative to ignore file only"""
        # /.env should only match .env at root level
        self._assert_ignored("/.env", ".env", True)

        # /build should only match build at root level
        self._assert_ignored("/build", "build", True)

        # Create a subdirectory scenario to test it doesn't match in subdirs
        (self.test_dir / "subproject").mkdir(exist_ok=True)
        (self.test_dir / "subproject" / ".env").write_text("content")
        self._assert_ignored("/.env", "subproject/.env", False)

    def test_patterns_with_slash_in_middle(self):
        """Test patterns with slash in middle - relative to ignore file"""
        # src/*.pyc should match .pyc files only in src directory
        self._assert_ignored("src/*.pyc", "src/main.pyc", True)
        self._assert_ignored("src/*.pyc", "src/utils.py", False)
        self._assert_ignored("src/*.pyc", "main.pyc", False)  # Not in src/

        # build/debug should match the debug directory inside build
        self._assert_ignored("build/debug", "build/debug", True)
        self._assert_ignored("build/debug", "build/output.o", False)

        # docs/images/* should match everything in docs/images/
        self._assert_ignored("docs/images/*", "docs/images/logo.png", True)
        self._assert_ignored("docs/images/*", "docs/readme.txt", False)

    def test_single_wildcard_with_slash(self):
        """Test patterns like */__pycache__ that require a parent directory"""
        # */__pycache__ should match __pycache__ only when it has a parent
        self._assert_ignored("*/__pycache__", "sub/__pycache__", True)
        self._assert_ignored("*/__pycache__", "__pycache__", False)  # No parent

        # */temp.tmp should match temp.tmp only in immediate subdirectories (one level)
        # Note: single * only matches one directory level, not multiple
        self._assert_ignored("*/temp.tmp", "sub/temp.tmp", True)
        self._assert_ignored("*/temp.tmp", "temp.tmp", False)  # At root level

    def test_double_asterisk_patterns(self):
        """Test ** patterns that match any number of directories"""
        # **/__pycache__ should match __pycache__ at any level
        self._assert_ignored("**/__pycache__", "__pycache__", True)
        self._assert_ignored("**/__pycache__", "sub/__pycache__", True)

        # **/node_modules should match node_modules anywhere
        self._assert_ignored("**/node_modules", "node_modules", True)

        # **/*.log should match .log files anywhere
        self._assert_ignored("**/*.log", "logs/error.log", True)
        self._assert_ignored("**/*.log", "logs/debug/trace.log", True)

        # build/** should match everything inside build
        self._assert_ignored("build/**", "build/output.o", True)
        self._assert_ignored("build/**", "build/debug/info.log", True)
        self._assert_ignored("build/**", "build", False)  # build itself, not contents
        self._assert_ignored("build/**", "src/main.py", False)  # Outside build

    def test_complex_double_asterisk_patterns(self):
        """Test complex ** patterns with multiple components"""
        # a/**/b should match a/b, a/x/b, a/x/y/b, etc.
        (self.test_dir / "a" / "b").mkdir(parents=True, exist_ok=True)
        (self.test_dir / "a" / "x" / "b").mkdir(parents=True, exist_ok=True)
        (self.test_dir / "a" / "x" / "y" / "b").mkdir(parents=True, exist_ok=True)

        self._assert_ignored("a/**/b", "a/b", True)
        self._assert_ignored("a/**/b", "a/x/b", True)
        self._assert_ignored("a/**/b", "a/x/y/b", True)
        self._assert_ignored("a/**/b", "a/x", False)
        self._assert_ignored("a/**/b", "x/b", False)

    def test_question_mark_patterns(self):
        """Test ? wildcard that matches single character except /"""
        # file?.txt should match file1.txt, fileA.txt but not file/txt
        (self.test_dir / "file1.txt").write_text("content")
        (self.test_dir / "fileA.txt").write_text("content")
        (self.test_dir / "file" / "txt").mkdir(parents=True, exist_ok=True)

        self._assert_ignored("file?.txt", "file1.txt", True)
        self._assert_ignored("file?.txt", "fileA.txt", True)
        self._assert_ignored("file?.txt", "file/txt", False)  # ? doesn't match /

    def test_character_ranges(self):
        """Test character range patterns [a-z], [0-9], etc."""
        # [0-9].txt should match digit filenames
        (self.test_dir / "1.txt").write_text("content")
        (self.test_dir / "9.txt").write_text("content")
        (self.test_dir / "a.txt").write_text("content")

        self._assert_ignored("[0-9].txt", "1.txt", True)
        self._assert_ignored("[0-9].txt", "9.txt", True)
        self._assert_ignored("[0-9].txt", "a.txt", False)

    def test_multiple_patterns(self):
        """Test ignore file with multiple patterns"""
        patterns = """
        # Python files
        *.pyc
        __pycache__/
        
        # Build artifacts
        build/
        dist/
        
        # Environment
        .env
        
        # Logs
        *.log
        """

        self._create_ignore_file(patterns)

        # Test each pattern works
        self.assertTrue(FileSystemService.is_ignored(self.test_dir / "file.pyc"))
        self.assertTrue(FileSystemService.is_ignored(self.test_dir / "__pycache__"))
        self.assertTrue(FileSystemService.is_ignored(self.test_dir / "build"))
        self.assertTrue(FileSystemService.is_ignored(self.test_dir / "dist"))
        self.assertTrue(FileSystemService.is_ignored(self.test_dir / ".env"))
        self.assertTrue(FileSystemService.is_ignored(self.test_dir / "logs/error.log"))

        # Test non-matching files
        self.assertFalse(FileSystemService.is_ignored(self.test_dir / "main.py"))
        self.assertFalse(FileSystemService.is_ignored(self.test_dir / "README.md"))

    def test_comments_and_blank_lines(self):
        """Test that comments and blank lines are ignored"""
        patterns = """
        # This is a comment
        *.pyc
        
        # Another comment
        
        __pycache__/
        """

        self._create_ignore_file(patterns)

        self.assertTrue(FileSystemService.is_ignored(self.test_dir / "file.pyc"))
        self.assertTrue(FileSystemService.is_ignored(self.test_dir / "__pycache__"))
        self.assertFalse(FileSystemService.is_ignored(self.test_dir / "main.py"))

    def test_non_existent_files(self):
        """Test that patterns work for non-existent files/directories"""
        self._assert_ignored("*.pyc", "nonexistent.pyc", True)
        # Note: Directory patterns with trailing slash only work if the directory exists
        # or if the path is provided with a trailing slash (which Path objects don't support)
        # so we skip testing non-existent directories here
        self._assert_ignored("build/", "nonexistent_build", False)

        # Test with files that have extensions but don't exist
        self._assert_ignored("*.js", "app.js", True)
        self._assert_ignored("*.js", "app.py", False)

    def test_edge_cases(self):
        """Test various edge cases"""
        # Empty pattern should not match anything
        self._assert_ignored("", "any_file.txt", False)

        # Single dot - in real gitignore, this would be unusual
        # Our implementation treats it as a literal filename match
        (self.test_dir / "dotfile").write_text(
            "content"
        )  # Create a file named just "dotfile"
        self._assert_ignored(".", "dotfile", False)  # Adjusted expectation

        # Pattern with only slash
        self._assert_ignored("/", "/", False)  # Should not match

        # Very long paths
        long_path = "a/" * 50 + "file.txt"
        (self.test_dir / long_path).parent.mkdir(parents=True, exist_ok=True)
        (self.test_dir / long_path).write_text("content")
        self._assert_ignored("**/*.txt", long_path, True)

    def test_make_ignore_regex(self):
        """Test the internal regex generation method if it exists"""
        # Test basic regex patterns
        patterns = ["*.pyc", "__pycache__/", "build/*"]
        for pattern in patterns:
            self._create_ignore_file(pattern)
            # Basic smoke test - if the method exists and doesn't crash
            try:
                # This tests that the pattern parsing doesn't break
                FileSystemService.is_ignored(self.test_dir / "test.pyc")
            except Exception as e:
                self.fail(f"Pattern '{pattern}' caused error: {e}")

    def test_make_ignore_regex_2(self):
        """Test more complex regex generation scenarios"""
        # Test patterns that might cause regex issues
        complex_patterns = [
            "**/*.{js,css,html}",  # If brace expansion is supported
            "file[0-9].txt",  # Character classes
            "path/with spaces/*.txt",  # Paths with spaces
            "special_chars_*.@#$.txt",  # Special characters
        ]

        for pattern in complex_patterns:
            self._create_ignore_file(pattern)
            # Test that complex patterns don't break the system
            try:
                result = FileSystemService.is_ignored(self.test_dir / "test.txt")
                # Just ensure it returns a boolean without crashing
                self.assertIsInstance(result, bool)
            except Exception:
                # Some patterns might not be supported, that's OK
                # But the function shouldn't crash
                pass


class TestMonorepoIgnoring(TestCase):
    """Tests for ignore behavior when the project sits inside an outer git repo."""

    def setUp(self):
        self.original_cwd = Path.cwd()
        self.tmp = Path(mkdtemp())
        self.monorepo = self.tmp / "monorepo"
        self.monorepo.mkdir()
        # Outer git repo, NOT at the abstra project root.
        subprocess.run(["git", "init"], cwd=self.monorepo, capture_output=True)
        subprocess.run(
            ["git", "config", "user.email", "test@test.com"],
            cwd=self.monorepo,
            capture_output=True,
        )
        subprocess.run(
            ["git", "config", "user.name", "Test User"],
            cwd=self.monorepo,
            capture_output=True,
        )

        self.project = self.monorepo / "abstra_project"
        self.project.mkdir()
        Settings.set_root_path(str(self.project.absolute()))

        (self.project / GITIGNORE_FILEPATH).write_text(".env\n")
        (self.project / ".env").write_text("SECRET=1")
        (self.project / "main.py").write_text("print('hi')")

        FileSystemService.clear_gitignore_cache()

    def tearDown(self):
        os.chdir(self.original_cwd)
        FileSystemService.clear_gitignore_cache()
        shutil.rmtree(self.tmp, ignore_errors=True)

    def test_is_git_available_rejects_outer_repo(self):
        # The fix: project root is not its own git repo, so we should NOT
        # route through `git check-ignore`.
        self.assertFalse(FileSystemService._is_git_available())

    def test_dotenv_in_inner_gitignore_is_ignored(self):
        # The original bug: would return False here, leaking `.env` into
        # the deploy bundle and overriding pod env vars in production.
        self.assertTrue(FileSystemService.is_ignored(self.project / ".env"))
        self.assertFalse(FileSystemService.is_ignored(self.project / "main.py"))

    def test_list_files_excludes_dotenv_in_monorepo(self):
        files = FileSystemService.list_files(self.project)
        names = {f.name for f in files}
        self.assertNotIn(".env", names)
        self.assertIn("main.py", names)
