"""
Tests for comment preservation functionality.

This module tests the comment extraction, metadata storage, and restoration
capabilities for IWFM input files.
"""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from pyiwfm.io.comment_extractor import (
    CommentExtractor,
    LineType,
    extract_and_save_comments,
    extract_comments,
)
from pyiwfm.io.comment_metadata import (
    CommentMetadata,
    FileCommentMetadata,
    PreserveMode,
    SectionComments,
)
from pyiwfm.io.comment_writer import (
    CommentInjector,
    CommentWriter,
)

# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture
def sample_iwfm_content():
    """Sample IWFM file content with various comment types."""
    return """\
C*******************************************************************************
C
C                  INTEGRATED WATER FLOW MODEL (IWFM)
C                        Version 2025.0.1747
C
C*******************************************************************************
C
C                            MAIN INPUT FILE
C                        for IWFM Pre-Processing
C
C             Generated by pyiwfm
C
C*****************************************************************************
C                     Titles Printed in the Output
C
C   *A Maximum of 3 title lines can be printed.
C-----------------------------------------------------------------------------
                                  Test Model
                              Generated by pyiwfm
                                   v2025.0.1747
C-----------------------------------------------------------------------------
C*****************************************************************************
C                            File Description
C-----------------------------------------------------------------------------
100                                 / NNODES
1.0                                 / FACTXY
C
C   ID              X              Y
C-----------------------------------------------------------------------------
     1   1000000.000000   2000000.000000  / SW corner boundary
     2   1001000.000000   2000000.000000
     3   1001000.000000   2001000.000000  / NE corner boundary
     4   1000000.000000   2001000.000000
C
C   Element data follows
C-----------------------------------------------------------------------------
"""


@pytest.fixture
def sample_metadata():
    """Sample CommentMetadata for testing."""
    meta = CommentMetadata(
        source_file="Preprocessor.in",
        iwfm_version="2025.0.1747",
        preserve_mode=PreserveMode.FULL,
        header_block=[
            "C*******************************************************************************",
            "C",
            "C                  INTEGRATED WATER FLOW MODEL (IWFM)",
            "C                        Custom Header",
            "C",
            "C*******************************************************************************",
        ],
    )
    meta.sections["NODES"] = SectionComments(
        section_name="NODES",
        header_comments=["C  Node data section", "C-------------"],
        inline_comments={"NNODES": "/ NNODES"},
        data_comments={"node:1": "SW corner", "node:157": "Stream intersection"},
    )
    return meta


# =============================================================================
# Test PreserveMode Enum
# =============================================================================


class TestPreserveMode:
    """Tests for PreserveMode enum."""

    def test_none_mode(self):
        assert PreserveMode.NONE.value == "none"

    def test_headers_mode(self):
        assert PreserveMode.HEADERS.value == "headers"

    def test_full_mode(self):
        assert PreserveMode.FULL.value == "full"


# =============================================================================
# Test SectionComments
# =============================================================================


class TestSectionComments:
    """Tests for SectionComments dataclass."""

    def test_basic_creation(self):
        section = SectionComments(section_name="NODES")
        assert section.section_name == "NODES"
        assert section.header_comments == []
        assert section.inline_comments == {}
        assert section.data_comments == {}
        assert section.trailing_comments == []

    def test_full_creation(self):
        section = SectionComments(
            section_name="ELEMENTS",
            header_comments=["C  Element section"],
            inline_comments={"NELEM": "/ element count"},
            data_comments={"elem:1": "First element"},
            trailing_comments=["C  End of elements"],
        )
        assert len(section.header_comments) == 1
        assert len(section.inline_comments) == 1
        assert len(section.data_comments) == 1
        assert len(section.trailing_comments) == 1

    def test_has_comments_empty(self):
        section = SectionComments(section_name="EMPTY")
        assert not section.has_comments()

    def test_has_comments_with_header(self):
        section = SectionComments(
            section_name="TEST",
            header_comments=["C  comment"],
        )
        assert section.has_comments()

    def test_has_comments_with_inline(self):
        section = SectionComments(
            section_name="TEST",
            inline_comments={"key": "value"},
        )
        assert section.has_comments()

    def test_get_data_comment(self):
        section = SectionComments(
            section_name="NODES",
            data_comments={"node:1": "First node", "node:2": "Second node"},
        )
        assert section.get_data_comment("node", 1) == "First node"
        assert section.get_data_comment("node", 2) == "Second node"
        assert section.get_data_comment("node", 999) is None

    def test_set_data_comment(self):
        section = SectionComments(section_name="NODES")
        section.set_data_comment("node", 42, "Test comment")
        assert section.data_comments["node:42"] == "Test comment"

    def test_to_dict(self):
        section = SectionComments(
            section_name="TEST",
            header_comments=["C  header"],
            inline_comments={"key": "value"},
        )
        d = section.to_dict()
        assert d["section_name"] == "TEST"
        assert d["header_comments"] == ["C  header"]
        assert d["inline_comments"] == {"key": "value"}

    def test_from_dict(self):
        data = {
            "section_name": "TEST",
            "header_comments": ["C  header"],
            "inline_comments": {"key": "value"},
            "data_comments": {},
            "trailing_comments": [],
        }
        section = SectionComments.from_dict(data)
        assert section.section_name == "TEST"
        assert section.header_comments == ["C  header"]


# =============================================================================
# Test CommentMetadata
# =============================================================================


class TestCommentMetadata:
    """Tests for CommentMetadata dataclass."""

    def test_basic_creation(self):
        meta = CommentMetadata()
        assert meta.version == "1.0"
        assert meta.source_file == ""
        assert meta.preserve_mode == PreserveMode.FULL
        assert meta.header_block == []
        assert meta.sections == {}

    def test_full_creation(self, sample_metadata):
        assert sample_metadata.source_file == "Preprocessor.in"
        assert sample_metadata.iwfm_version == "2025.0.1747"
        assert "NODES" in sample_metadata.sections

    def test_has_comments_empty(self):
        meta = CommentMetadata()
        assert not meta.has_comments()

    def test_has_comments_with_header(self):
        meta = CommentMetadata(header_block=["C  header"])
        assert meta.has_comments()

    def test_has_comments_with_section(self):
        meta = CommentMetadata()
        meta.sections["TEST"] = SectionComments(
            section_name="TEST",
            header_comments=["C  comment"],
        )
        assert meta.has_comments()

    def test_get_section(self, sample_metadata):
        section = sample_metadata.get_section("NODES")
        assert section is not None
        assert section.section_name == "NODES"

        missing = sample_metadata.get_section("NONEXISTENT")
        assert missing is None

    def test_get_or_create_section(self):
        meta = CommentMetadata()
        section = meta.get_or_create_section("NEW_SECTION")
        assert section is not None
        assert section.section_name == "NEW_SECTION"
        assert "NEW_SECTION" in meta.sections

        # Second call returns same section
        same_section = meta.get_or_create_section("NEW_SECTION")
        assert same_section is section

    def test_to_dict(self, sample_metadata):
        d = sample_metadata.to_dict()
        assert d["version"] == "1.0"
        assert d["source_file"] == "Preprocessor.in"
        assert d["preserve_mode"] == "full"
        assert "NODES" in d["sections"]

    def test_from_dict(self):
        data = {
            "version": "1.0",
            "source_file": "test.in",
            "iwfm_version": "2025",
            "preserve_mode": "headers",
            "header_block": ["C  header"],
            "sections": {},
            "file_metadata": {},
        }
        meta = CommentMetadata.from_dict(data)
        assert meta.source_file == "test.in"
        assert meta.preserve_mode == PreserveMode.HEADERS

    def test_save_and_load(self, sample_metadata):
        with tempfile.TemporaryDirectory() as tmpdir:
            path = Path(tmpdir) / "test_metadata.json"
            sample_metadata.save(path)

            assert path.exists()

            loaded = CommentMetadata.load(path)
            assert loaded is not None
            assert loaded.source_file == sample_metadata.source_file
            assert loaded.iwfm_version == sample_metadata.iwfm_version
            assert "NODES" in loaded.sections

    def test_load_nonexistent(self):
        loaded = CommentMetadata.load(Path("/nonexistent/path.json"))
        assert loaded is None

    def test_sidecar_path(self):
        source = Path("C:/models/Preprocessor.in")
        sidecar = CommentMetadata.sidecar_path(source)
        assert sidecar == Path("C:/models/Preprocessor.in.iwfm_comments.json")

    def test_load_for_file(self, sample_metadata):
        with tempfile.TemporaryDirectory() as tmpdir:
            source = Path(tmpdir) / "test.in"
            source.write_text("test content")

            sidecar = sample_metadata.sidecar_path(source)
            sample_metadata.save(sidecar)

            loaded = CommentMetadata.load_for_file(source)
            assert loaded is not None
            assert loaded.source_file == sample_metadata.source_file

    def test_save_for_file(self, sample_metadata):
        with tempfile.TemporaryDirectory() as tmpdir:
            source = Path(tmpdir) / "test.in"
            source.write_text("test content")

            sidecar_path = sample_metadata.save_for_file(source)
            assert sidecar_path.exists()
            assert sidecar_path.name == "test.in.iwfm_comments.json"

    def test_merge(self):
        meta1 = CommentMetadata(
            header_block=["C  header 1"],
        )
        meta1.sections["SEC1"] = SectionComments(
            section_name="SEC1",
            header_comments=["C  sec1 header"],
        )

        meta2 = CommentMetadata(
            header_block=["C  header 2"],
        )
        meta2.sections["SEC2"] = SectionComments(
            section_name="SEC2",
            header_comments=["C  sec2 header"],
        )

        meta1.merge(meta2)

        assert len(meta1.header_block) == 2
        assert "SEC1" in meta1.sections
        assert "SEC2" in meta1.sections


# =============================================================================
# Test FileCommentMetadata
# =============================================================================


class TestFileCommentMetadata:
    """Tests for FileCommentMetadata container."""

    def test_basic_creation(self):
        fcm = FileCommentMetadata()
        assert fcm.files == {}

    def test_get_set(self):
        fcm = FileCommentMetadata()
        meta = CommentMetadata(source_file="test.in")

        fcm.set("preprocessor_main", meta)
        assert fcm.get("preprocessor_main") is meta
        assert fcm.get("nonexistent") is None

    def test_has_comments_empty(self):
        fcm = FileCommentMetadata()
        assert not fcm.has_comments()

    def test_has_comments_with_content(self):
        fcm = FileCommentMetadata()
        meta = CommentMetadata(header_block=["C  header"])
        fcm.set("test", meta)
        assert fcm.has_comments()


# =============================================================================
# Test CommentExtractor
# =============================================================================


class TestCommentExtractor:
    """Tests for CommentExtractor class."""

    def test_line_classification_full_line_comment_c(self):
        extractor = CommentExtractor()
        pl = extractor._classify_line(1, "C  This is a comment\n", "C  This is a comment")
        assert pl.line_type == LineType.FULL_LINE_COMMENT

    def test_line_classification_full_line_comment_lowercase(self):
        extractor = CommentExtractor()
        pl = extractor._classify_line(1, "c  lowercase\n", "c  lowercase")
        assert pl.line_type == LineType.FULL_LINE_COMMENT

    def test_line_classification_full_line_comment_asterisk(self):
        extractor = CommentExtractor()
        pl = extractor._classify_line(1, "*  asterisk\n", "*  asterisk")
        assert pl.line_type == LineType.FULL_LINE_COMMENT

    def test_line_classification_banner(self):
        extractor = CommentExtractor()
        pl = extractor._classify_line(1, "C***********************\n", "C***********************")
        assert pl.line_type == LineType.BANNER

    def test_line_classification_inline_comment(self):
        extractor = CommentExtractor()
        pl = extractor._classify_line(1, "100  / inline\n", "100  / inline")
        assert pl.line_type == LineType.INLINE_COMMENT
        assert pl.data_part == "100"
        assert pl.comment_part == "/ inline"

    def test_line_classification_data(self):
        extractor = CommentExtractor()
        pl = extractor._classify_line(1, "100\n", "100")
        assert pl.line_type == LineType.DATA

    def test_line_classification_blank(self):
        extractor = CommentExtractor()
        pl = extractor._classify_line(1, "\n", "")
        assert pl.line_type == LineType.BLANK

    def test_extract_inline_comment(self):
        extractor = CommentExtractor()

        # With inline comment
        data, comment = extractor._extract_inline_comment("100  / comment")
        assert data == "100"
        assert comment == "/ comment"

        # Without inline comment
        data, comment = extractor._extract_inline_comment("100")
        assert data == "100"
        assert comment is None

        # Slash in date (not inline comment)
        data, comment = extractor._extract_inline_comment("09/30/1990")
        assert data == "09/30/1990"
        assert comment is None

        # DSS pathname (not inline comment)
        data, comment = extractor._extract_inline_comment(" /A/B/C//E/F/")
        assert data == " /A/B/C//E/F/"
        assert comment is None

    def test_extract_keyword(self):
        extractor = CommentExtractor()

        assert extractor._extract_keyword("/ NNODES") == "NNODES"
        assert extractor._extract_keyword("/ 1: PREPROCESSOR FILE") == "PREPROCESSOR FILE"
        assert extractor._extract_keyword("just a comment") == ""

    def test_extract_from_file(self, sample_iwfm_content):
        with tempfile.TemporaryDirectory() as tmpdir:
            filepath = Path(tmpdir) / "test.in"
            filepath.write_text(sample_iwfm_content)

            extractor = CommentExtractor()
            metadata = extractor.extract(filepath)

            assert metadata.source_file == "test.in"
            assert len(metadata.header_block) > 0
            assert "IWFM" in " ".join(metadata.header_block)

    def test_extract_detects_version(self, sample_iwfm_content):
        with tempfile.TemporaryDirectory() as tmpdir:
            filepath = Path(tmpdir) / "test.in"
            filepath.write_text(sample_iwfm_content)

            extractor = CommentExtractor()
            metadata = extractor.extract(filepath)

            assert "2025" in metadata.iwfm_version

    def test_extract_file_not_found(self):
        extractor = CommentExtractor()
        with pytest.raises(FileNotFoundError):
            extractor.extract(Path("/nonexistent/file.in"))


# =============================================================================
# Test CommentWriter
# =============================================================================


class TestCommentWriter:
    """Tests for CommentWriter class."""

    def test_restore_header(self, sample_metadata):
        writer = CommentWriter(sample_metadata)
        header = writer.restore_header()
        assert "IWFM" in header
        assert "Custom Header" in header

    def test_restore_header_fallback(self):
        writer = CommentWriter(None)
        fallback = ["C  Fallback header"]
        header = writer.restore_header(fallback)
        assert "Fallback header" in header

    def test_restore_header_no_fallback(self):
        writer = CommentWriter(None, use_fallback=False)
        header = writer.restore_header()
        assert header == ""

    def test_restore_section_header(self, sample_metadata):
        writer = CommentWriter(sample_metadata)
        section_header = writer.restore_section_header("NODES")
        assert "Node data section" in section_header

    def test_restore_section_header_missing(self, sample_metadata):
        writer = CommentWriter(sample_metadata)
        section_header = writer.restore_section_header("NONEXISTENT")
        assert section_header == ""

    def test_format_data_with_comment(self, sample_metadata):
        writer = CommentWriter(sample_metadata)
        # Using inline_comments key
        line = writer.format_data_with_comment("100", "NODES", "NNODES")
        assert "NNODES" in line

    def test_format_value_with_keyword(self, sample_metadata):
        writer = CommentWriter(sample_metadata)
        line = writer.format_value_with_keyword("100", "NNODES", "NODES")
        assert "100" in line
        assert "NNODES" in line

    def test_has_preserved_comments(self, sample_metadata):
        writer = CommentWriter(sample_metadata)
        assert writer.has_preserved_comments()

        empty_writer = CommentWriter(None)
        assert not empty_writer.has_preserved_comments()


# =============================================================================
# Test CommentInjector
# =============================================================================


class TestCommentInjector:
    """Tests for CommentInjector class."""

    def test_inject_header(self, sample_metadata):
        injector = CommentInjector(sample_metadata)

        content = """\
C***********************
C  Old Header
C***********************
100 / NNODES
"""
        result = injector.inject_header(content)
        assert "Custom Header" in result

    def test_inject_header_no_metadata(self):
        injector = CommentInjector(None)
        content = "C  test\n100\n"
        result = injector.inject_header(content)
        assert result == content


# =============================================================================
# Test Convenience Functions
# =============================================================================


class TestConvenienceFunctions:
    """Tests for module-level convenience functions."""

    def test_extract_comments(self, sample_iwfm_content):
        with tempfile.TemporaryDirectory() as tmpdir:
            filepath = Path(tmpdir) / "test.in"
            filepath.write_text(sample_iwfm_content)

            metadata = extract_comments(filepath)
            assert metadata is not None
            assert len(metadata.header_block) > 0

    def test_extract_and_save_comments(self, sample_iwfm_content):
        with tempfile.TemporaryDirectory() as tmpdir:
            filepath = Path(tmpdir) / "test.in"
            filepath.write_text(sample_iwfm_content)

            sidecar = extract_and_save_comments(filepath)
            assert sidecar.exists()
            assert sidecar.name == "test.in.iwfm_comments.json"


# =============================================================================
# Test Integration with Base Classes
# =============================================================================


class TestCommentAwareClasses:
    """Tests for CommentAwareReader and CommentAwareWriter base classes."""

    def test_comment_aware_reader_import(self):
        from pyiwfm.io.base import CommentAwareReader

        assert CommentAwareReader is not None

    def test_comment_aware_writer_import(self):
        from pyiwfm.io.base import CommentAwareWriter

        assert CommentAwareWriter is not None

    def test_template_writer_with_comments(self, sample_metadata):
        from pyiwfm.io.writer_base import TemplateWriter

        # Create a concrete implementation for testing
        class TestWriter(TemplateWriter):
            def write(self, data):
                pass

            @property
            def format(self):
                return "test"

        with tempfile.TemporaryDirectory() as tmpdir:
            writer = TestWriter(tmpdir, comment_metadata=sample_metadata)
            assert writer.has_preserved_comments()
            assert writer.comment_metadata is sample_metadata


# =============================================================================
# Test JSON Serialization
# =============================================================================


class TestJSONSerialization:
    """Tests for JSON serialization round-trip."""

    def test_full_roundtrip(self, sample_metadata):
        """Test complete serialization and deserialization."""
        with tempfile.TemporaryDirectory() as tmpdir:
            path = Path(tmpdir) / "metadata.json"

            # Save
            sample_metadata.save(path)

            # Verify JSON content
            with open(path) as f:
                data = json.load(f)
            assert data["version"] == "1.0"
            assert data["source_file"] == "Preprocessor.in"
            assert data["preserve_mode"] == "full"

            # Load and compare
            loaded = CommentMetadata.load(path)
            assert loaded.source_file == sample_metadata.source_file
            assert loaded.iwfm_version == sample_metadata.iwfm_version
            assert len(loaded.header_block) == len(sample_metadata.header_block)

            # Check sections
            nodes = loaded.sections.get("NODES")
            assert nodes is not None
            assert nodes.get_data_comment("node", 1) == "SW corner"
