import logging
import os
import tempfile
import unittest

import biotite.structure as struc
import biotite.structure.io as strucio  # Use this for load_structure
import numpy as np

from synth_pdb.data import (
    BOND_LENGTH_C_O,
    BOND_LENGTH_CA_C,
    BOND_LENGTH_N_CA,
    STANDARD_AMINO_ACIDS,
)
from synth_pdb.generator import _resolve_sequence, generate_pdb_content
from synth_pdb.validator import PDBValidator

# Suppress logging during tests to keep output clean
logging.getLogger().setLevel(logging.CRITICAL)


class TestGenerator(unittest.TestCase):

    def _parse_atom_line(self, line: str) -> dict:
        """Parses an ATOM PDB line and returns a dictionary of atom properties."""
        return {
            "record_name": line[0:6].strip(),
            "atom_number": int(line[6:11].strip()),
            "atom_name": line[12:16].strip(),
            "alt_loc": line[16].strip(),
            "residue_name": line[17:20].strip(),
            "chain_id": line[21].strip(),
            "residue_number": int(line[22:26].strip()),
            "insertion_code": line[26].strip(),
            "x_coord": float(line[30:38]),
            "y_coord": float(line[38:46]),
            "z_coord": float(line[46:54]),
            "occupancy": float(line[54:60]),
            "temp_factor": float(line[60:66]),
            "element": line[76:78].strip(),
            "charge": line[78:80].strip(),
        }

    # --- Tests for _get_sequence ---
    def test_get_sequence_random_length(self):
        """Test if random sequence generation has the correct length."""
        for length in [1, 5, 10, 100]:
            sequence = _resolve_sequence(length=length, user_sequence_str=None)
            self.assertEqual(len(sequence), length)

    def test_generate_pdb_content_full_atom_backbone_geometry(self):
        """
        Test if the N, CA, C, O backbone atom coordinates for a single residue
        adhere to the defined bond lengths and angles from data.py.
        """
        # Test with a single Alanine residue
        content = generate_pdb_content(sequence_str="ALA")

        # Save to a temporary file and load with biotite to get AtomArray
        with tempfile.NamedTemporaryFile(mode="w", suffix=".pdb", delete=False) as temp_pdb_file:
            temp_pdb_file.write(content)
            temp_file_path = temp_pdb_file.name

        try:
            atom_array = strucio.load_structure(temp_file_path)  # Removed format="pdb"
            if isinstance(atom_array, struc.AtomArrayStack):
                atom_array = atom_array[0]  # Take the first model if it's a stack

            # Extract coordinates for N, CA, C, O
            n_coord = atom_array[atom_array.atom_name == "N"].coord[0]
            ca_coord = atom_array[atom_array.atom_name == "CA"].coord[0]
            c_coord = atom_array[atom_array.atom_name == "C"].coord[0]
            o_coord = atom_array[atom_array.atom_name == "O"].coord[0]

            # Verify bond lengths
            # N-CA bond length
            dist_n_ca = np.linalg.norm(n_coord - ca_coord)
            self.assertAlmostEqual(
                dist_n_ca, BOND_LENGTH_N_CA, places=1, msg="N-CA bond length mismatch"
            )

            # CA-C bond length
            dist_ca_c = np.linalg.norm(ca_coord - c_coord)
            self.assertAlmostEqual(
                dist_ca_c, BOND_LENGTH_CA_C, places=1, msg="CA-C bond length mismatch"
            )

            # C-O bond length
            dist_c_o = np.linalg.norm(c_coord - o_coord)
            self.assertAlmostEqual(
                dist_c_o, BOND_LENGTH_C_O, places=1, msg="C-O bond length mismatch"
            )

            # Verify angles (optional, as the focus is on bond lengths here)
            # Helper to calculate angle between three points (B is vertex)
            def calculate_angle(A, B, C):
                BA = A - B
                BC = C - B
                cosine_angle = np.dot(BA, BC) / (np.linalg.norm(BA) * np.linalg.norm(BC))
                angle = np.degrees(np.arccos(cosine_angle))
                return angle

            # N-CA-C angle
            calculate_angle(n_coord, ca_coord, c_coord)
            # self.assertAlmostEqual(angle_n_ca_c, ANGLE_N_CA_C, places=1, msg="N-CA-C angle mismatch")

            # CA-C-O angle
            calculate_angle(ca_coord, c_coord, o_coord)
            # self.assertAlmostEqual(angle_ca_c_o, ANGLE_CA_C_O, places=1, msg="CA-C-O angle mismatch")
        finally:
            os.remove(temp_file_path)  # Clean up the temporary file

    def test_get_sequence_random_empty(self):
        """Test random empty sequence request."""
        sequence = _resolve_sequence(length=0, user_sequence_str=None)
        self.assertEqual(len(sequence), 0)
        sequence = _resolve_sequence(length=-5, user_sequence_str=None)
        self.assertEqual(len(sequence), 0)

    def test_get_sequence_random_amino_acids(self):
        """Test if all elements in random sequence are valid amino acids."""
        sequence = _resolve_sequence(length=20, user_sequence_str=None)
        for aa in sequence:
            self.assertIn(aa, STANDARD_AMINO_ACIDS)

    def test_get_sequence_from_1_letter_code(self):
        """Test parsing of a valid 1-letter code sequence."""
        sequence_str = "AGV"
        expected_sequence = ["ALA", "GLY", "VAL"]
        content = generate_pdb_content(sequence_str=sequence_str)  # Removed full_atom
        atom_lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]
        # ALA=13, GLY=10, VAL=19. Total expected for AGV was 42 with internal OXT.
        # With OXT and terminal hydrogens (H2, H3) removed from internal residues (ALA, GLY),
        # we expect (13-1-2) + (10-1-2) + 19 = 10 + 7 + 19 = 36.
        self.assertEqual(len(atom_lines), 36)

        sequence = _resolve_sequence(
            length=0, user_sequence_str=sequence_str
        )  # length should be ignored
        self.assertEqual(sequence, expected_sequence)

    def test_get_sequence_from_3_letter_code(self):
        """Test parsing of a valid 3-letter code sequence."""
        sequence_str = "ALA-GLY-VAL"
        content = generate_pdb_content(sequence_str=sequence_str)  # Removed full_atom
        atom_lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]
        # ALA=13, GLY=10, VAL=19. Total expected for AGV was 42 with internal OXT.
        # With OXT and terminal hydrogens (H2, H3) removed from internal residues (ALA, GLY),
        # we expect (13-1-2) + (10-1-2) + 19 = 10 + 7 + 19 = 36.
        self.assertEqual(len(atom_lines), 36)  # Check total atom lines

    def test_reproducibility_with_seed(self):
        """Test that using a fixed seed produces identical output."""
        sequence = "ACDEF"
        seed = 42

        # Run 1
        pdb1 = generate_pdb_content(sequence_str=sequence, conformation="random", seed=seed)

        # Run 2
        pdb2 = generate_pdb_content(sequence_str=sequence, conformation="random", seed=seed)

        # Run 3 (different seed)
        pdb3 = generate_pdb_content(sequence_str=sequence, conformation="random", seed=seed + 1)

        self.assertEqual(pdb1, pdb2, "Same seed should produce identical PDB content")
        self.assertNotEqual(
            pdb1,
            pdb3,
            "Different seeds should produce different PDB content (for random conformer)",
        )

    def test_generate_pdb_content_large_scale_stress_test(self):
        """Verify that the generator handles very large proteins without JIT/Memory issues."""
        sequence = "A" * 500
        try:
            pdb_content = generate_pdb_content(sequence_str=sequence)
            self.assertIn("ATOM   4000", pdb_content)  # Should have > 4000 atoms
            self.assertIn("TER", pdb_content)
        except Exception as e:
            self.fail(f"Large scale generation failed: {e}")

    def test_numba_precision_consistency(self):
        """Verify that JITted geometry results match basic stability requirements."""
        import numpy as np

        from synth_pdb.geometry import position_atom_3d_from_internal_coords

        p1 = np.array([0.0, 0.0, 0.0], dtype=np.float64)
        p2 = np.array([1.5, 0.0, 0.0], dtype=np.float64)
        p3 = np.array([2.5, 1.2, 0.0], dtype=np.float64)

        # Get JIT result
        jit_res = position_atom_3d_from_internal_coords(p1, p2, p3, 1.5, 110.0, 180.0)

        # Verify basic properties (dtype, stability)
        self.assertFalse(np.any(np.isnan(jit_res)))
        self.assertEqual(jit_res.dtype, np.float64)

    def test_get_sequence_from_mixed_case(self):
        """Test parsing of mixed-case sequence strings."""
        sequence_str_1 = "aGv"
        expected_sequence_1 = ["ALA", "GLY", "VAL"]
        self.assertEqual(
            _resolve_sequence(length=0, user_sequence_str=sequence_str_1), expected_sequence_1
        )

        sequence_str_2 = "Ala-GlY-vAl"
        expected_sequence_2 = ["ALA", "GLY", "VAL"]
        self.assertEqual(
            _resolve_sequence(length=0, user_sequence_str=sequence_str_2), expected_sequence_2
        )

    def test_get_sequence_invalid_1_letter_code(self):
        """Test handling of invalid 1-letter code sequence."""
        sequence_str = "AXG"
        with self.assertRaisesRegex(ValueError, "Invalid 1-letter amino acid code: X"):
            _resolve_sequence(length=0, user_sequence_str=sequence_str)

    def test_get_sequence_invalid_3_letter_code(self):
        """Test handling of invalid 3-letter code sequence."""
        sequence_str = "ALA-XYZ-VAL"
        with self.assertRaisesRegex(ValueError, "Invalid 3-letter amino acid code: XYZ"):
            _resolve_sequence(length=0, user_sequence_str=sequence_str)

    def test_get_sequence_plausible_frequencies(self):
        """
        Test if random sequence generation with plausible frequencies
        adheres to the expected distribution within a tolerance.
        """
        from synth_pdb.data import AMINO_ACID_FREQUENCIES

        test_length = 10000
        tolerance = 0.02  # 2% deviation allowed

        sequence = _resolve_sequence(length=test_length, use_plausible_frequencies=True)
        self.assertEqual(len(sequence), test_length)

        # Calculate observed frequencies
        observed_counts = {aa: sequence.count(aa) for aa in AMINO_ACID_FREQUENCIES.keys()}
        observed_frequencies = {aa: count / test_length for aa, count in observed_counts.items()}

        # Compare observed with expected frequencies
        for aa, expected_freq in AMINO_ACID_FREQUENCIES.items():
            observed_freq = observed_frequencies.get(aa, 0.0)
            self.assertAlmostEqual(
                observed_freq,
                expected_freq,
                delta=tolerance,
                msg=f"Frequency for {aa} (Observed: {observed_freq:.4f}, Expected: {expected_freq:.4f}) out of tolerance.",
            )

    # --- Tests for generate_pdb_content (general) ---
    def test_generate_pdb_content_empty_length(self):
        """Test PDB content generation for zero or negative length when no sequence is provided."""
        with self.assertRaisesRegex(
            ValueError, "Length must be a positive integer when no sequence is provided."
        ):
            generate_pdb_content(length=0, sequence_str=None)
        with self.assertRaisesRegex(
            ValueError, "Length must be a positive integer when no sequence is provided."
        ):
            generate_pdb_content(length=-5, sequence_str=None)

    def test_generate_pdb_content_empty_sequence_str_raises_error(self):
        """Test PDB content generation with an empty sequence string raises ValueError."""
        with self.assertRaisesRegex(ValueError, "Provided sequence string cannot be empty."):
            generate_pdb_content(length=0, sequence_str="")

    # --- Refactored tests for unified generate_pdb_content (full-atom) ---
    def test_generate_pdb_content_num_lines(self):
        """Test if the generated PDB content has the correct number of ATOM lines for full-atom."""
        # The number of atoms for ALA according to biotite.structure.info.residue("ALA", "C_TERM") is 13
        content = generate_pdb_content(sequence_str="ALA")
        atom_lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]
        self.assertEqual(len(atom_lines), 13)

        # The number of atoms for GLY according to biotite.structure.info.residue("GLY", "C_TERM") is 10
        content = generate_pdb_content(sequence_str="GLY")
        atom_lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]
        self.assertEqual(len(atom_lines), 10)

    def test_generate_pdb_content_coordinates_backbone_present(self):
        """Test if key backbone atoms (N, CA, C, O) are present and have coordinates."""
        content = generate_pdb_content(sequence_str="ALA")
        with tempfile.NamedTemporaryFile(mode="w", suffix=".pdb", delete=False) as temp_pdb_file:
            temp_pdb_file.write(content)
            temp_file_path = temp_pdb_file.name
        try:
            atom_array = strucio.load_structure(temp_file_path)
            if isinstance(atom_array, struc.AtomArrayStack):
                atom_array = atom_array[0]

            self.assertTrue(np.any(atom_array.atom_name == "N"))
            self.assertTrue(np.any(atom_array.atom_name == "CA"))
            self.assertTrue(np.any(atom_array.atom_name == "C"))
            self.assertTrue(np.any(atom_array.atom_name == "O"))
        finally:
            os.remove(temp_file_path)

    def test_generate_pdb_content_atom_residue_numbers(self):
        """Test if atom and residue numbers are sequential for full-atom."""
        length = 3
        content = generate_pdb_content(length=length)

        with tempfile.NamedTemporaryFile(mode="w", suffix=".pdb", delete=False) as temp_pdb_file:
            temp_pdb_file.write(content)
            temp_file_path = temp_pdb_file.name
        try:
            atom_array = strucio.load_structure(temp_file_path)
            if isinstance(atom_array, struc.AtomArrayStack):
                atom_array = atom_array[0]

            self.assertGreater(atom_array.array_length(), 0, "No atoms found for peptide.")

            atom_array.res_id[0]
            for atom_idx in range(
                1, atom_array.array_length()
            ):  # Start from 1 to avoid comparing with itself
                # Only check if the residue changes, then the new residue ID should be greater or equal
                if atom_array.res_id[atom_idx] != atom_array.res_id[atom_idx - 1]:
                    self.assertEqual(
                        atom_array.res_id[atom_idx],
                        atom_array.res_id[atom_idx - 1] + 1,
                        f"Residue ID not sequential: {atom_array.res_id[atom_idx-1]} -> {atom_array.res_id[atom_idx]}",
                    )

            # Check if total number of unique residues matches the peptide length
            unique_res_ids = np.unique(atom_array.res_id)
            self.assertEqual(
                len(unique_res_ids),
                length,
                f"Expected {length} unique residues, but found {len(unique_res_ids)}",
            )
        finally:
            os.remove(temp_file_path)

    def test_generate_pdb_content_residue_names(self):
        """Test if residue names are valid for full-atom."""
        length = 5
        content = generate_pdb_content(length=length)

        with tempfile.NamedTemporaryFile(mode="w", suffix=".pdb", delete=False) as temp_pdb_file:
            temp_pdb_file.write(content)
            temp_file_path = temp_pdb_file.name
        try:
            atom_array = strucio.load_structure(temp_file_path)
            if isinstance(atom_array, struc.AtomArrayStack):
                atom_array = atom_array[0]

            for res_name in atom_array.res_name:
                valid_residues = STANDARD_AMINO_ACIDS + ["HIE", "HID", "HIP"]
                self.assertIn(res_name, valid_residues)
        finally:
            os.remove(temp_file_path)

    def test_generate_pdb_content_full_atom_more_atoms(self):
        """Test that generated content for ALA has the expected number of atoms."""
        # For a single ALA, we expect 13 atoms
        content = generate_pdb_content(sequence_str="ALA")
        atom_lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]
        self.assertEqual(len(atom_lines), 13, "Expected 13 atoms for ALA (full atom).")

    def test_generate_pdb_content_full_atom_backbone_atoms(self):
        """Test for the presence of N, C, O backbone atoms in full_atom mode."""
        length = 1
        content = generate_pdb_content(length=length)
        lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]

        atom_names = {
            self._parse_atom_line(line)["atom_name"] for line in lines
        }  # Extract atom names

        self.assertIn("N", atom_names)  # Check for unpadded name
        self.assertIn("CA", atom_names)  # Check for unpadded name
        self.assertIn("C", atom_names)  # Check for unpadded name
        self.assertIn("O", atom_names)  # Check for unpadded name

    def test_linear_full_atom_peptide_is_ramachandran_valid(self):
        """
        Test that a generated full-atom peptide, using the Ramachandran-guided geometry,
        exhibits NO Ramachandran violations.
        """
        # Generate a short peptide, full atom mode
        content = generate_pdb_content(length=5, sequence_str="AAAAA")

        validator = PDBValidator(pdb_content=content)
        validator.validate_ramachandran()
        violations = validator.get_violations()

        # Expecting valid geometry now
        self.assertEqual(len(violations), 0, "Expected NO Ramachandran violations, but found some.")

        # Optionally, print violations for debugging purposes if the test fails unexpectedly
        if not violations:
            print(
                "No Ramachandran violations found. This might indicate an issue with the test setup or validator."
            )
        else:
            print(f"Found {len(violations)} Ramachandran violations (expected for linear chain):")
            for violation in violations:
                print(f"- {violation}")

    # --- Tests for PDB Header, TER, END records ---
    def test_generate_pdb_content_no_unintended_blank_lines(self):
        """Test that there are no unintended blank lines in the PDB content."""
        content = generate_pdb_content(length=5)
        lines = content.split("\n")

        non_trailing_blank_lines_count = 0
        for i, line in enumerate(lines):
            # Only count blank lines that are not the very last element (potential trailing newline from .join)
            if not line.strip() and i < len(lines) - 1:
                non_trailing_blank_lines_count += 1

        # The test should FAIL if it finds any unintended blank lines.
        # We expect 0 unintended blank lines.
        self.assertEqual(
            non_trailing_blank_lines_count,
            0,
            f"Found {non_trailing_blank_lines_count} unintended blank lines. Content:\n{content}",
        )

        # Remove the assertion for total lines, as it's now dynamic and complex to assert directly.
        # non_empty_lines = [line for line in lines if line.strip()]
        # expected_content_lines = 19
        # self.assertEqual(len(non_empty_lines), expected_content_lines,
        #                  f"Expected {expected_content_lines} non-empty lines, but found {len(non_empty_lines)}. Content:\n{content}")

    def test_generate_pdb_content_header_present(self):
        """Test if the PDB header is present at the beginning."""
        content = generate_pdb_content(length=1)
        lines = content.split("\n")
        self.assertTrue(lines[0].startswith("HEADER"))
        self.assertTrue(lines[1].startswith("TITLE"))

    def test_generate_pdb_content_ter_present(self):
        """Test if the TER record is present and correctly formatted."""
        length = 3
        content = generate_pdb_content(length=length)
        lines = content.strip().split("\n")

        ter_line = [line for line in lines if line.startswith("TER")][-1]
        self.assertIsNotNone(ter_line)
        self.assertTrue(ter_line.startswith("TER"))

        # Parse TER line directly as its format is different from ATOM
        # TER   atom_ser resName chainID resSeq
        # 0123456789012345678901234567890
        # TER   601      LEU A  100
        ter_atom_num = int(ter_line[6:11].strip())
        ter_res_name = ter_line[17:20].strip()
        ter_chain_id = ter_line[21].strip()
        ter_res_num = int(ter_line[22:26].strip())

        self.assertEqual(ter_chain_id, "A", "Chain ID in TER record should be 'A'")

        # Extract last atom number from the preceding ATOM line
        atom_lines = [line for line in lines if line.startswith("ATOM")]
        last_atom_line = atom_lines[-1]
        atom_data_last = self._parse_atom_line(last_atom_line)

        # The TER record atom number should be one greater than the last ATOM record
        self.assertEqual(ter_atom_num, atom_data_last["atom_number"] + 1)

        # Check residue name and number of the TER record matches the last ATOM record
        self.assertEqual(ter_res_name, atom_data_last["residue_name"])
        self.assertEqual(ter_res_num, atom_data_last["residue_number"])

    def test_generate_pdb_content_end_present(self):
        """Test if the END record is present at the very end."""
        content = generate_pdb_content(length=1)
        lines = content.strip().split("\n")
        self.assertEqual(lines[-1], "END")

    # --- New tests for generate_pdb_content with sequence_str ---
    def test_generate_pdb_content_with_sequence_1_letter(self):
        """Test PDB content generation with a user-provided 1-letter sequence."""
        sequence_str = "AGV"
        expected_sequence = ["ALA", "GLY", "VAL"]
        content = generate_pdb_content(sequence_str=sequence_str)
        atom_lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]

        # Count residues based on distinct residue numbers
        parsed_residues = []
        for line in atom_lines:
            atom_data = self._parse_atom_line(line)
            if atom_data["residue_number"] not in [r["residue_number"] for r in parsed_residues]:
                parsed_residues.append(atom_data)

        self.assertEqual(
            len(parsed_residues),
            len(expected_sequence),
            "Number of parsed residues does not match expected sequence length.",
        )
        for i, res_data in enumerate(parsed_residues):
            self.assertEqual(res_data["residue_name"], expected_sequence[i])

    def test_generate_pdb_content_with_sequence_3_letter(self):
        """Test PDB content generation with a user-provided 3-letter sequence."""
        sequence_str = "ALA-GLY-VAL"
        expected_sequence = ["ALA", "GLY", "VAL"]
        content = generate_pdb_content(sequence_str=sequence_str)
        atom_lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]

        # Count residues based on distinct residue numbers
        parsed_residues = []
        for line in atom_lines:
            atom_data = self._parse_atom_line(line)
            if atom_data["residue_number"] not in [r["residue_number"] for r in parsed_residues]:
                parsed_residues.append(atom_data)

        self.assertEqual(
            len(parsed_residues),
            len(expected_sequence),
            "Number of parsed residues does not match expected sequence length.",
        )
        for i, res_data in enumerate(parsed_residues):
            self.assertEqual(res_data["residue_name"], expected_sequence[i])

    def test_generate_pdb_content_sequence_overrides_length(self):
        """Test that provided sequence's length overrides the 'length' parameter."""
        sequence_str = "AG"  # Length 2
        length_param = 5  # Should be ignored
        content = generate_pdb_content(length=length_param, sequence_str=sequence_str)
        atom_lines = [line for line in content.strip().split("\n") if line.startswith("ATOM")]

        # Count residues based on distinct residue numbers
        parsed_residues = []
        for line in atom_lines:
            atom_data = self._parse_atom_line(line)
            if atom_data["residue_number"] not in [r["residue_number"] for r in parsed_residues]:
                parsed_residues.append(atom_data)

        self.assertEqual(len(parsed_residues), 2)  # Should be 2, not 5

    def test_generate_pdb_content_invalid_sequence_str_raises_error(self):
        """Test that invalid sequence string raises ValueError during PDB generation."""
        invalid_sequence_str = "AXG"
        with self.assertRaises(ValueError):
            generate_pdb_content(sequence_str=invalid_sequence_str)

    def test_generate_pdb_content_pdb_atom_format_compliance(self):
        """
        Test if the generated ATOM lines comply with PDB format specifications
        regarding field widths, justifications, and data types.
        """
        test_cases = [
            (1, "Full-atom"),  # Single residue, full-atom
            (5, "Full-atom Multi"),  # Multiple residues, full-atom
        ]

        # Regex for float with 3 decimal places and 8 width: ^ {1}\d\.\d{3}$ or ^ {2}\.\d{3}$ or ^ {1}\d{2}\.\d{3}$
        # Generally, it's float_str = f"{value:8.3f}". The space padding is implicit.
        # So we check for 8 characters total, with 3 after the decimal point.
        # Adjusted regex to handle potential leading space/minus sign before digits more flexibly
        COORD_REGEX = r"^\s*[-]?\d{1,3}\.\d{3}$"  # Allows for optional spaces/minus, 1-3 digits before decimal, 3 after
        OCC_TEMP_REGEX = r"^\s*[-]?\d{1,2}\.\d{2}$"  # Allows for optional spaces/minus, 1-2 digits before decimal, 2 after

        for length, description in test_cases:
            with self.subTest(f"Testing {description} (length={length})"):
                content = generate_pdb_content(length=length)
                atom_lines = [
                    line for line in content.strip().split("\n") if line.startswith("ATOM")
                ]

                self.assertGreater(len(atom_lines), 0, "No ATOM lines found.")

                for i, line in enumerate(atom_lines):
                    self.assertEqual(len(line), 80, f"Line length not 80 for line {i+1}: '{line}'")

                    atom_data = self._parse_atom_line(line)

                    # --- Verify string field lengths and justifications ---
                    # Atom name (13-16) - 4 chars
                    # Biotite's PDB writer often outputs single-character atom names like " N  " (space, N, two spaces)
                    # and two-character atom names like " CA " (space, CA, space).
                    # We need to reflect that in the test expectation.
                    if len(atom_data["atom_name"]) == 1:
                        # For single-char atom names, expect " N  "
                        self.assertEqual(
                            line[12:16],
                            " " + atom_data["atom_name"] + "  ",
                            f"Atom name '{atom_data['atom_name']}' padding incorrect: '{line[12:16]}'",
                        )
                    elif len(atom_data["atom_name"]) == 2:
                        # For two-char atom names, expect " CA "
                        self.assertEqual(
                            line[12:16],
                            " " + atom_data["atom_name"] + " ",
                            f"Atom name '{atom_data['atom_name']}' padding incorrect: '{line[12:16]}'",
                        )
                    elif len(atom_data["atom_name"]) == 3:
                        # For three-char atom names, expect " CD1" (space, CD1)
                        self.assertEqual(
                            line[12:16],
                            " " + atom_data["atom_name"],
                            f"Atom name '{atom_data['atom_name']}' padding incorrect: '{line[12:16]}'",
                        )
                    else:  # For four-char atom names, expect exact match
                        self.assertEqual(
                            line[12:16],
                            atom_data["atom_name"],
                            f"Atom name '{atom_data['atom_name']}' padding incorrect: '{line[12:16]}'",
                        )

                    # Remaining checks (unchanged from previous iterations)
                    self.assertEqual(
                        atom_data["record_name"], "ATOM", "Record name should be 'ATOM'"
                    )
                    self.assertIsInstance(atom_data["atom_number"], int)
                    self.assertIsInstance(atom_data["residue_number"], int)
                    self.assertIsInstance(atom_data["x_coord"], float)
                    self.assertIsInstance(atom_data["y_coord"], float)
                    self.assertIsInstance(atom_data["z_coord"], float)
                    self.assertIsInstance(atom_data["occupancy"], float)
                    self.assertIsInstance(atom_data["temp_factor"], float)

                    # Atom number (6-11)
                    self.assertEqual(len(line[6:11]), 5)
                    self.assertTrue(line[6:11].strip().isdigit())

                    # Residue name (18-20) - 3 chars, right justified
                    self.assertEqual(len(line[17:20]), 3)
                    self.assertEqual(line[17:20].strip(), atom_data["residue_name"])

                    # Chain ID (22) - 1 char
                    self.assertEqual(len(line[21]), 1)
                    self.assertEqual(
                        atom_data["chain_id"], "A", msg=f"Chain ID mismatch in line: {line}"
                    )

                    # Residue number (23-26) - 4 chars, right justified
                    self.assertEqual(len(line[22:26]), 4)
                    self.assertTrue(line[22:26].strip().isdigit())
                    self.assertEqual(int(line[22:26]), atom_data["residue_number"])

                    # Coordinates (31-38, 39-46, 47-54) - 8 chars each, 3 decimal places
                    self.assertEqual(len(line[30:38]), 8)
                    self.assertEqual(len(line[38:46]), 8)
                    self.assertEqual(len(line[46:54]), 8)

                    self.assertRegex(
                        line[30:38], COORD_REGEX, f"X coord format incorrect: '{line[30:38]}'"
                    )
                    self.assertRegex(
                        line[38:46], COORD_REGEX, f"Y coord format incorrect: '{line[38:46]}'"
                    )
                    self.assertRegex(
                        line[46:54], COORD_REGEX, f"Z coord format incorrect: '{line[46:54]}'"
                    )

                    # Occupancy (55-60) - 6 chars, 2 decimal places
                    self.assertEqual(len(line[54:60]), 6)
                    self.assertRegex(
                        line[54:60], OCC_TEMP_REGEX, f"Occupancy format incorrect: '{line[54:60]}'"
                    )
                    # Occupancy should now be realistic (0.85-1.00), not hardcoded 1.00
                    self.assertGreaterEqual(
                        atom_data["occupancy"], 0.85, "Occupancy should be >= 0.85"
                    )
                    self.assertLessEqual(
                        atom_data["occupancy"], 1.00, "Occupancy should be <= 1.00"
                    )

                    self.assertEqual(len(line[60:66]), 6)
                    self.assertRegex(
                        line[60:66],
                        OCC_TEMP_REGEX,
                        f"Temp factor format incorrect: '{line[60:66]}'",
                    )
                    # B-factors should now be realistic (5-100 Ų), not 0.00
                    # Updated for Model-Free physics (Termini can be highly flexible)
                    temp_factor = float(line[60:66])
                    self.assertGreaterEqual(temp_factor, 5.00, "B-factor should be >= 5.00")
                    self.assertLessEqual(temp_factor, 100.00, "B-factor should be <= 100.00")

                    # Element (77-78) - 2 chars, right justified
                    self.assertEqual(len(line[76:78]), 2)
                    self.assertEqual(line[76:78].strip(), atom_data["element"])

                    # Charge (79-80) - 2 chars
                    self.assertEqual(len(line[78:80]), 2)
                    # Implementation may generate charges like '1+' for N-term or '' for neutral
                    self.assertIn(atom_data["charge"], ["", "1+", "1-"])

    def test_generate_pdb_content_long_peptide_numbering_and_chain_id(self):
        """
        Test if atom and residue numbering, and chain ID are correct for longer peptides
        in full-atom mode.
        """
        peptide_length = 10
        # Now generate_pdb_content always produces full-atom
        content = generate_pdb_content(length=peptide_length)

        # Save to a temporary file and load with biotite to get AtomArray
        with tempfile.NamedTemporaryFile(mode="w", suffix=".pdb", delete=False) as temp_pdb_file:
            temp_pdb_file.write(content)
            temp_file_path = temp_pdb_file.name

        try:
            atom_array = strucio.load_structure(temp_file_path)
            if isinstance(atom_array, struc.AtomArrayStack):
                atom_array = atom_array[0]  # Take the first model if it's a stack

            self.assertGreater(atom_array.array_length(), 0, "No atoms found for long peptide.")

            current_res_id = atom_array.res_id[0]  # Initialize with the first residue ID
            expected_residue_count = 1  # Start count from 1 for the first residue

            for atom_idx in range(atom_array.array_length()):
                # Atom number should be sequential and unique
                # The atom_array.atom_id attribute is not guaranteed to be sequential,
                # so we will check that the residue numbers are sequential.
                # self.assertEqual(atom_array.atom_id[atom_idx], last_atom_num + 1,
                #                  f"Atom number not sequential at index {atom_idx}.")
                # last_atom_num = atom_array.atom_id[atom_idx]

                # Chain ID should always be 'A'
                self.assertEqual(
                    atom_array.chain_id[atom_idx], "A", f"Chain ID not 'A' at index {atom_idx}."
                )

                # Residue number should be sequential
                if atom_array.res_id[atom_idx] != current_res_id:  # Check if residue changed
                    current_res_id = atom_array.res_id[atom_idx]
                    expected_residue_count += 1
                self.assertEqual(
                    atom_array.res_id[atom_idx],
                    current_res_id,  # Corrected variable name
                    f"Residue number not consistent within a residue block at index {atom_idx}.",
                )

            # Check if total number of unique residues matches the peptide length
            unique_res_ids = np.unique(atom_array.res_id)  # Define unique_res_ids here
            self.assertEqual(
                len(unique_res_ids),
                peptide_length,
                f"Expected {peptide_length} unique residues, but found {len(unique_res_ids)}",
            )
        finally:
            os.remove(temp_file_path)  # Clean up the temporary file


if __name__ == "__main__":
    unittest.main()
