"""Comprehensive unit tests for DeepAnalyzerCore.

Tests the core deep analysis engine: AnalysisResult dataclass,
DeepAnalyzerCore initialization, preference/SWE analysis helpers,
report generation methods, AI agent layer generation, and the
full analyze() orchestrator with extensive mocking.
"""

import json
import os
import tempfile
import unittest
from dataclasses import dataclass, field
from unittest.mock import MagicMock, patch

from datarecipe.core.deep_analyzer import AnalysisResult, DeepAnalyzerCore

# ==================== Stub / Helper Objects ====================


@dataclass
class StubAllocation:
    """Minimal allocation stub matching HumanMachineAllocation interface."""

    tasks: list = field(default_factory=lambda: [MagicMock(), MagicMock()])
    total_human_hours: float = 100.0
    total_human_cost: float = 2500.0
    total_machine_cost: float = 500.0
    total_cost: float = 3000.0
    human_work_percentage: float = 70.0
    machine_work_percentage: float = 30.0


@dataclass
class StubRubricsResult:
    total_rubrics: int = 50
    unique_patterns: int = 10
    verb_distribution: dict = field(default_factory=dict)
    structured_patterns: list = field(default_factory=list)


@dataclass
class StubPromptLibrary:
    unique_count: int = 5


@dataclass
class StubStrategyResult:
    synthetic_score: float = 0.6
    modified_score: float = 0.3


@dataclass
class StubDomain:
    value: str = "general"


@dataclass
class StubComplexityMetrics:
    primary_domain: object = field(default_factory=lambda: StubDomain())
    difficulty_score: float = 2.5
    time_multiplier: float = 1.2
    cost_multiplier: float = 1.3
    quality_requirement: str = "standard"

    def to_dict(self):
        return {
            "primary_domain": self.primary_domain.value,
            "difficulty_score": self.difficulty_score,
            "time_multiplier": self.time_multiplier,
            "cost_multiplier": self.cost_multiplier,
        }


@dataclass
class StubEnhancedContext:
    generated: bool = True
    dataset_purpose_summary: str = "A test dataset"
    key_methodology_insights: list = field(default_factory=lambda: ["insight1"])
    competitive_positioning: str = "Strong positioning"
    domain_specific_tips: list = field(default_factory=lambda: ["tip1"])
    reproduction_strategy: str = "Reproduce by X"
    tailored_risks: list = field(
        default_factory=lambda: [
            {"level": "high", "description": "Risk A", "mitigation": "Mitigate A"}
        ]
    )


@dataclass
class StubLLMAnalysis:
    dataset_type: str = "instruction"
    purpose: str = "Test purpose"
    structure_description: str = "Structured data"
    key_fields: list = field(default_factory=list)
    production_steps: list = field(default_factory=list)
    quality_criteria: list = field(default_factory=list)
    estimated_difficulty: str = "medium"
    similar_datasets: list = field(default_factory=list)


@dataclass
class StubTokenStats:
    def to_dict(self):
        return {"avg_tokens": 100, "total_tokens": 5000}


@dataclass
class StubPreciseEstimate:
    adjusted_cost: float = 150.0
    token_stats: object = field(default_factory=StubTokenStats)

    def to_dict(self):
        return {"adjusted_cost": self.adjusted_cost, "token_stats": self.token_stats.to_dict()}


@dataclass
class StubCalibrationResult:
    calibrated_human_cost: float = 2400.0
    calibrated_api_cost: float = 480.0
    calibration_method: str = "historical"
    confidence: float = 0.8
    based_on_datasets: list = field(default_factory=list)
    cost_range_low: float = 2000.0
    cost_range_high: float = 3500.0

    def to_dict(self):
        return {
            "calibrated_human_cost": self.calibrated_human_cost,
            "calibrated_api_cost": self.calibrated_api_cost,
        }


@dataclass
class StubPhasedBreakdown:
    grand_total: float = 3500.0

    def to_dict(self):
        return {"grand_total": self.grand_total, "phases": []}


@dataclass
class StubRecipeSummary:
    pass


# ==================== AnalysisResult Tests ====================


class TestAnalysisResult(unittest.TestCase):
    """Test AnalysisResult dataclass."""

    def test_default_values(self):
        r = AnalysisResult(dataset_id="test/ds")
        self.assertEqual(r.dataset_id, "test/ds")
        self.assertTrue(r.success)
        self.assertEqual(r.error, "")
        self.assertEqual(r.dataset_type, "")
        self.assertEqual(r.sample_count, 0)
        self.assertEqual(r.fields, [])
        self.assertEqual(r.reproduction_cost, {})
        self.assertEqual(r.human_percentage, 0.0)
        self.assertEqual(r.rubric_patterns, 0)
        self.assertEqual(r.prompt_templates, 0)
        self.assertEqual(r.output_dir, "")
        self.assertEqual(r.files_generated, [])
        self.assertEqual(r.warnings, [])
        self.assertEqual(r.enhancement_prompt, "")

    def test_to_dict(self):
        r = AnalysisResult(
            dataset_id="org/ds",
            success=True,
            dataset_type="evaluation",
            sample_count=100,
            fields=["a", "b"],
            reproduction_cost={"total": 1000.0},
            human_percentage=60.0,
            rubric_patterns=5,
            prompt_templates=3,
            output_dir="/tmp/out",
            files_generated=["f1.md"],
            warnings=["w1"],
        )
        d = r.to_dict()
        self.assertEqual(d["dataset_id"], "org/ds")
        self.assertTrue(d["success"])
        self.assertEqual(d["dataset_type"], "evaluation")
        self.assertEqual(d["sample_count"], 100)
        self.assertEqual(d["fields"], ["a", "b"])
        self.assertEqual(d["reproduction_cost"], {"total": 1000.0})
        self.assertEqual(d["human_percentage"], 60.0)
        self.assertEqual(d["rubric_patterns"], 5)
        self.assertEqual(d["prompt_templates"], 3)
        self.assertEqual(d["output_dir"], "/tmp/out")
        self.assertEqual(d["files_generated"], ["f1.md"])
        self.assertEqual(d["warnings"], ["w1"])
        # enhancement_prompt should NOT be in to_dict
        self.assertNotIn("enhancement_prompt", d)

    def test_to_dict_defaults(self):
        d = AnalysisResult(dataset_id="x").to_dict()
        self.assertFalse(d["error"])
        self.assertEqual(d["warnings"], [])

    def test_mutable_defaults_isolation(self):
        """Ensure mutable default fields are independent between instances."""
        r1 = AnalysisResult(dataset_id="a")
        r2 = AnalysisResult(dataset_id="b")
        r1.fields.append("field1")
        r1.warnings.append("warn1")
        self.assertEqual(r2.fields, [])
        self.assertEqual(r2.warnings, [])


# ==================== DeepAnalyzerCore __init__ Tests ====================


class TestDeepAnalyzerCoreInit(unittest.TestCase):
    """Test DeepAnalyzerCore initialization."""

    def test_default_init(self):
        core = DeepAnalyzerCore()
        self.assertEqual(core.output_dir, "./projects")
        self.assertEqual(core.region, "china")
        self.assertFalse(core.use_llm)
        self.assertEqual(core.llm_provider, "anthropic")
        self.assertEqual(core.enhance_mode, "auto")
        self.assertIsNone(core.pre_enhanced_context)

    def test_custom_init(self):
        ctx = StubEnhancedContext()
        core = DeepAnalyzerCore(
            output_dir="/custom/out",
            region="us",
            use_llm=True,
            llm_provider="openai",
            enhance_mode="api",
            pre_enhanced_context=ctx,
        )
        self.assertEqual(core.output_dir, "/custom/out")
        self.assertEqual(core.region, "us")
        self.assertTrue(core.use_llm)
        self.assertEqual(core.llm_provider, "openai")
        self.assertEqual(core.enhance_mode, "api")
        self.assertIs(core.pre_enhanced_context, ctx)


# ==================== Preference Pair Analysis Tests ====================


class TestAnalyzePreferencePair(unittest.TestCase):
    """Test _analyze_preference_pair method."""

    def setUp(self):
        self.core = DeepAnalyzerCore()

    def test_non_string_chosen_rejected_skips(self):
        item = {"chosen": 123, "rejected": 456}
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertEqual(len(pairs), 0)
        self.assertEqual(topics, {})

    def test_basic_preference_pair(self):
        item = {
            "chosen": "\n\nHuman: Hello\n\nAssistant: Hi there, how can I help you?",
            "rejected": "\n\nHuman: Hello\n\nAssistant: Hi",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        # Should detect a topic and length pattern
        self.assertGreater(len(topics), 0)
        self.assertEqual(len(pairs), 1)
        self.assertIn("topic", pairs[0])
        self.assertIn("turn_count", pairs[0])

    def test_coding_topic_detection(self):
        item = {
            "chosen": "\n\nHuman: Write python code for sorting\n\nAssistant: Here is the code...",
            "rejected": "\n\nHuman: Write python code for sorting\n\nAssistant: No.",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertIn("coding", topics)

    def test_creative_writing_topic(self):
        item = {
            "chosen": "\n\nHuman: Write a story about a cat\n\nAssistant: Once upon a time...",
            "rejected": "\n\nHuman: Write a story about a cat\n\nAssistant: No.",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertIn("creative_writing", topics)

    def test_explanation_topic(self):
        item = {
            "chosen": "\n\nHuman: Explain what is quantum physics\n\nAssistant: Quantum physics is...",
            "rejected": "\n\nHuman: Explain what is quantum physics\n\nAssistant: Idk.",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertIn("explanation", topics)

    def test_advice_topic(self):
        item = {
            "chosen": "\n\nHuman: Can you help me with my resume?\n\nAssistant: Sure, here are some tips...",
            "rejected": "\n\nHuman: Can you help me with my resume?\n\nAssistant: No.",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertIn("advice", topics)

    def test_translation_topic(self):
        item = {
            "chosen": "\n\nHuman: Translate this to chinese please\n\nAssistant: Here is the translation...",
            "rejected": "\n\nHuman: Translate this to chinese please\n\nAssistant: No.",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertIn("translation", topics)

    def test_length_patterns_chosen_longer(self):
        item = {
            "chosen": "\n\nHuman: Hi\n\nAssistant: " + "A" * 200,
            "rejected": "\n\nHuman: Hi\n\nAssistant: Short",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertEqual(patterns["chosen_longer"], 1)

    def test_length_patterns_rejected_longer(self):
        item = {
            "chosen": "\n\nHuman: Hi\n\nAssistant: Short",
            "rejected": "\n\nHuman: Hi\n\nAssistant: " + "B" * 200,
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertEqual(patterns["rejected_longer"], 1)

    def test_length_patterns_same_length(self):
        item = {
            "chosen": "\n\nHuman: Hi\n\nAssistant: Same length response here",
            "rejected": "\n\nHuman: Hi\n\nAssistant: Same length response here",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertEqual(patterns["same_length"], 1)

    def test_safety_pattern(self):
        item = {
            "chosen": "\n\nHuman: Hi\n\nAssistant: Here is a helpful answer!",
            "rejected": "\n\nHuman: Hi\n\nAssistant: Sorry, I can't help with that. It's inappropriate.",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertEqual(patterns["chosen_safer"], 1)

    def test_pairs_limit_20(self):
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        for _ in range(25):
            item = {
                "chosen": "\n\nHuman: Hi\n\nAssistant: Hello",
                "rejected": "\n\nHuman: Hi\n\nAssistant: Bye",
            }
            self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertEqual(len(pairs), 20)

    def test_general_topic_fallback(self):
        """When no keyword matches, topic should be 'general'."""
        item = {
            "chosen": "\n\nHuman: Tell me the weather\n\nAssistant: It's sunny today.",
            "rejected": "\n\nHuman: Tell me the weather\n\nAssistant: No idea.",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        self.assertIn("general", topics)


# ==================== SWE Item Analysis Tests ====================


class TestAnalyzeSweItem(unittest.TestCase):
    """Test _analyze_swe_item method."""

    def setUp(self):
        self.core = DeepAnalyzerCore()

    def _empty_stats(self):
        return {
            "repos": {},
            "languages": {},
            "issue_types": {},
            "issue_categories": {},
            "patch_lines": [],
            "examples": [],
        }

    def test_basic_swe_item(self):
        stats = self._empty_stats()
        item = {
            "repo": "django/django",
            "repo_language": "Python",
            "patch": "+added line\n-removed line\ncontext line",
            "problem_statement": "Fix the bug in models",
        }
        self.core._analyze_swe_item(item, stats)
        self.assertEqual(stats["repos"]["django/django"], 1)
        self.assertEqual(stats["languages"]["Python"], 1)
        self.assertEqual(len(stats["patch_lines"]), 1)
        self.assertEqual(stats["patch_lines"][0], 2)  # +added and -removed
        self.assertEqual(len(stats["examples"]), 1)
        self.assertEqual(stats["examples"][0]["repo"], "django/django")

    def test_issue_specificity_parsing(self):
        stats = self._empty_stats()
        item = {
            "repo": "test/repo",
            "repo_language": "JavaScript",
            "patch": "+line",
            "problem_statement": "Test problem",
            "issue_specificity": "['bug', 'enhancement']",
        }
        self.core._analyze_swe_item(item, stats)
        self.assertEqual(stats["issue_types"].get("bug", 0), 1)
        self.assertEqual(stats["issue_types"].get("enhancement", 0), 1)

    def test_issue_specificity_invalid(self):
        stats = self._empty_stats()
        item = {
            "repo": "test/repo",
            "patch": "+line",
            "problem_statement": "Test",
            "issue_specificity": "not a list",
        }
        self.core._analyze_swe_item(item, stats)
        # Should not crash, issue_types stays empty
        self.assertEqual(stats["issue_types"], {})

    def test_examples_limit_5(self):
        stats = self._empty_stats()
        for i in range(8):
            item = {
                "repo": f"repo{i}",
                "patch": "+x",
                "problem_statement": f"Problem {i}",
            }
            self.core._analyze_swe_item(item, stats)
        self.assertEqual(len(stats["examples"]), 5)

    def test_missing_repo_language(self):
        stats = self._empty_stats()
        item = {
            "repo": "test/repo",
            "patch": "",
            "problem_statement": "Test",
        }
        self.core._analyze_swe_item(item, stats)
        self.assertEqual(stats["languages"]["unknown"], 1)

    def test_repo_count_increments(self):
        stats = self._empty_stats()
        for _ in range(3):
            item = {
                "repo": "same/repo",
                "patch": "+a",
                "problem_statement": "Test",
            }
            self.core._analyze_swe_item(item, stats)
        self.assertEqual(stats["repos"]["same/repo"], 3)

    def test_problem_statement_truncated(self):
        stats = self._empty_stats()
        item = {
            "repo": "test/repo",
            "patch": "+line",
            "problem_statement": "X" * 1000,
        }
        self.core._analyze_swe_item(item, stats)
        self.assertLessEqual(len(stats["examples"][0]["problem_statement"]), 800)


# ==================== Report Generation Tests ====================


class TestGenerateAnalysisReport(unittest.TestCase):
    """Test _generate_analysis_report method."""

    def setUp(self):
        self.core = DeepAnalyzerCore()
        self.allocation = StubAllocation()

    def test_basic_report(self):
        report = self.core._generate_analysis_report(
            dataset_id="test/ds",
            sample_count=100,
            actual_size=1000,
            rubrics_result=None,
            prompt_library=None,
            strategy_result=None,
            allocation=self.allocation,
            region="china",
        )
        self.assertIn("test/ds", report)
        self.assertIn("100", report)
        self.assertIn("1,000", report)
        self.assertIn("$2,500", report)
        self.assertIn("$500", report)
        self.assertIn("DataRecipe", report)

    def test_report_with_rubrics(self):
        rubrics = StubRubricsResult()
        report = self.core._generate_analysis_report(
            dataset_id="test/ds",
            sample_count=100,
            actual_size=1000,
            rubrics_result=rubrics,
            prompt_library=None,
            strategy_result=None,
            allocation=self.allocation,
            region="china",
        )
        self.assertIn("50", report)  # total_rubrics
        self.assertIn("10", report)  # unique_patterns

    def test_report_with_prompt_library(self):
        prompts = StubPromptLibrary()
        report = self.core._generate_analysis_report(
            dataset_id="test/ds",
            sample_count=100,
            actual_size=1000,
            rubrics_result=None,
            prompt_library=prompts,
            strategy_result=None,
            allocation=self.allocation,
            region="china",
        )
        self.assertIn("5", report)  # unique_count

    def test_report_with_strategy(self):
        strategy = StubStrategyResult()
        report = self.core._generate_analysis_report(
            dataset_id="test/ds",
            sample_count=100,
            actual_size=1000,
            rubrics_result=None,
            prompt_library=None,
            strategy_result=strategy,
            allocation=self.allocation,
            region="china",
        )
        self.assertIn("60%", report)  # synthetic_score * 100
        self.assertIn("30%", report)  # modified_score * 100

    def test_report_with_enhanced_context(self):
        ctx = StubEnhancedContext()
        report = self.core._generate_analysis_report(
            dataset_id="test/ds",
            sample_count=100,
            actual_size=1000,
            rubrics_result=None,
            prompt_library=None,
            strategy_result=None,
            allocation=self.allocation,
            region="china",
            enhanced_context=ctx,
        )
        self.assertIn("A test dataset", report)
        self.assertIn("insight1", report)
        self.assertIn("Strong positioning", report)
        self.assertIn("tip1", report)

    def test_report_enhanced_context_not_generated(self):
        ctx = StubEnhancedContext(generated=False)
        report = self.core._generate_analysis_report(
            dataset_id="test/ds",
            sample_count=100,
            actual_size=1000,
            rubrics_result=None,
            prompt_library=None,
            strategy_result=None,
            allocation=self.allocation,
            region="china",
            enhanced_context=ctx,
        )
        # Should NOT include enhanced sections when generated=False
        self.assertNotIn("insight1", report)
        self.assertNotIn("Strong positioning", report)


# ==================== Reproduction Guide Tests ====================


class TestGenerateReproductionGuide(unittest.TestCase):
    """Test _generate_reproduction_guide method."""

    def setUp(self):
        self.core = DeepAnalyzerCore()
        self.allocation = StubAllocation()
        self.schema_info = {"field_a": {"type": "str", "examples": []}}

    def _make_guide(self, **overrides):
        defaults = {
            "dataset_id": "test/ds",
            "schema_info": self.schema_info,
            "category_set": set(),
            "sub_category_set": set(),
            "system_prompts_by_domain": {},
            "rubrics_examples": [],
            "sample_items": [],
            "rubrics_result": None,
            "prompt_library": None,
            "allocation": self.allocation,
            "is_preference_dataset": False,
            "preference_pairs": [],
            "preference_topics": {},
            "preference_patterns": {},
            "is_swe_dataset": False,
            "swe_stats": {},
            "llm_analysis": None,
            "enhanced_context": None,
        }
        defaults.update(overrides)
        return self.core._generate_reproduction_guide(**defaults)

    def test_basic_guide(self):
        guide = self._make_guide()
        self.assertIn("test/ds", guide)
        self.assertIn("field_a", guide)
        self.assertIn("$2,500", guide)
        self.assertIn("DataRecipe", guide)

    def test_guide_swe_dataset(self):
        guide = self._make_guide(is_swe_dataset=True)
        self.assertIn("SWE-bench", guide)

    def test_guide_preference_dataset(self):
        guide = self._make_guide(is_preference_dataset=True)
        self.assertIn("RLHF", guide)

    def test_guide_with_enhanced_context(self):
        ctx = StubEnhancedContext()
        guide = self._make_guide(enhanced_context=ctx)
        self.assertIn("A test dataset", guide)
        self.assertIn("Reproduce by X", guide)
        self.assertIn("insight1", guide)
        self.assertIn("tip1", guide)
        self.assertIn("Risk A", guide)
        self.assertIn("Mitigate A", guide)

    def test_guide_with_llm_analysis_unknown_type(self):
        """LLM analysis with 'unknown' type should NOT generate LLM guide section."""
        llm = StubLLMAnalysis(dataset_type="unknown")
        guide = self._make_guide(llm_analysis=llm)
        # The LLM section header should not appear for unknown type
        self.assertNotIn("Test purpose", guide)

    def test_guide_enhanced_context_not_generated(self):
        ctx = StubEnhancedContext(generated=False)
        guide = self._make_guide(enhanced_context=ctx)
        self.assertNotIn("Reproduce by X", guide)
        self.assertNotIn("insight1", guide)

    def test_guide_no_allocation(self):
        """Even with allocation=None, cost section should not crash
        because the method checks `if allocation`."""
        # The method uses allocation without a None guard at the cost section,
        # but in practice allocation is always set. Test with a stub that has
        # zero costs for coverage.
        alloc = StubAllocation(total_human_cost=0, total_machine_cost=0, total_cost=0)
        guide = self._make_guide(allocation=alloc)
        self.assertIn("$0", guide)

    def test_guide_tailored_risks_non_dict_ignored(self):
        """Non-dict risk items should be handled gracefully."""
        ctx = StubEnhancedContext(tailored_risks=["simple string risk"])
        guide = self._make_guide(enhanced_context=ctx)
        # Should not crash; non-dict items just don't produce table rows
        self.assertIn("test/ds", guide)


# ==================== AI Agent Layer Tests ====================


class TestGenerateAiAgentLayer(unittest.TestCase):
    """Test AI agent layer generation methods."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        self.core = DeepAnalyzerCore(output_dir=self.tmpdir)

    def tearDown(self):
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def _make_output_mgr(self):
        from datarecipe.core.project_layout import OutputManager
        return OutputManager(
            self.tmpdir,
            subdirs=["decision", "project", "annotation", "guide", "cost", "data", "ai_agent"],
        )

    def test_generate_ai_agent_context(self):
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(
            dataset_id="test/ds",
            reproduction_cost={"total": 1000, "human": 700, "api": 300},
            human_percentage=70.0,
        )
        from datarecipe.core.project_layout import OUTPUT_SUBDIRS
        self.core._generate_ai_agent_context(
            output_mgr, result, "test/ds", "evaluation",
            100, 1000, StubAllocation(), StubComplexityMetrics(),
            OUTPUT_SUBDIRS,
        )
        path = output_mgr.get_path("ai_agent", "agent_context.json")
        self.assertTrue(os.path.exists(path))
        with open(path) as f:
            data = json.load(f)
        self.assertEqual(data["project"]["name"], "test/ds")
        self.assertEqual(data["project"]["type"], "evaluation")
        self.assertIsNotNone(data["complexity"])

    def test_generate_ai_agent_context_no_complexity(self):
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(
            dataset_id="test/ds",
            reproduction_cost={"total": 1000, "human": 700, "api": 300},
            human_percentage=70.0,
        )
        from datarecipe.core.project_layout import OUTPUT_SUBDIRS
        self.core._generate_ai_agent_context(
            output_mgr, result, "test/ds", "evaluation",
            100, 1000, StubAllocation(), None,
            OUTPUT_SUBDIRS,
        )
        path = output_mgr.get_path("ai_agent", "agent_context.json")
        with open(path) as f:
            data = json.load(f)
        self.assertIsNone(data["complexity"])

    def test_generate_ai_workflow_state(self):
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(dataset_id="test/ds")
        from datarecipe.core.project_layout import OUTPUT_SUBDIRS
        self.core._generate_ai_workflow_state(
            output_mgr, result, "test/ds", "evaluation", OUTPUT_SUBDIRS,
        )
        path = output_mgr.get_path("ai_agent", "workflow_state.json")
        self.assertTrue(os.path.exists(path))
        with open(path) as f:
            data = json.load(f)
        self.assertEqual(data["current_phase"], "analysis_complete")
        self.assertIn("phases", data)
        self.assertIn("next_actions", data)

    def test_generate_ai_reasoning_traces(self):
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(
            dataset_id="test/ds",
            reproduction_cost={"total": 1000, "human": 700, "api": 300},
            human_percentage=70.0,
        )
        from datarecipe.core.project_layout import OUTPUT_SUBDIRS
        self.core._generate_ai_reasoning_traces(
            output_mgr, result, "test/ds", "preference",
            1000, StubAllocation(), StubComplexityMetrics(),
            StubRubricsResult(), StubPromptLibrary(),
            OUTPUT_SUBDIRS,
        )
        path = output_mgr.get_path("ai_agent", "reasoning_traces.json")
        self.assertTrue(os.path.exists(path))
        with open(path) as f:
            data = json.load(f)
        self.assertEqual(
            data["reasoning"]["dataset_type"]["conclusion"]["value"], "preference"
        )
        # Preference type should have high confidence
        self.assertGreaterEqual(
            data["reasoning"]["dataset_type"]["confidence"], 0.9
        )

    def test_generate_ai_reasoning_traces_evaluation_type(self):
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(
            dataset_id="test/ds",
            reproduction_cost={"total": 500, "human": 300, "api": 200},
            human_percentage=60.0,
            rubric_patterns=5,
        )
        from datarecipe.core.project_layout import OUTPUT_SUBDIRS
        self.core._generate_ai_reasoning_traces(
            output_mgr, result, "test/ds", "evaluation",
            500, StubAllocation(), None,
            StubRubricsResult(), None,
            OUTPUT_SUBDIRS,
        )
        path = output_mgr.get_path("ai_agent", "reasoning_traces.json")
        with open(path) as f:
            data = json.load(f)
        self.assertEqual(
            data["reasoning"]["dataset_type"]["confidence"], 0.9
        )

    def test_generate_ai_reasoning_traces_swe_type(self):
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(
            dataset_id="test/ds",
            reproduction_cost={"total": 500, "human": 300, "api": 200},
            human_percentage=60.0,
        )
        from datarecipe.core.project_layout import OUTPUT_SUBDIRS
        self.core._generate_ai_reasoning_traces(
            output_mgr, result, "test/ds", "swe_bench",
            500, StubAllocation(), None, None, None,
            OUTPUT_SUBDIRS,
        )
        path = output_mgr.get_path("ai_agent", "reasoning_traces.json")
        with open(path) as f:
            data = json.load(f)
        self.assertEqual(
            data["reasoning"]["dataset_type"]["confidence"], 0.95
        )

    def test_generate_ai_pipeline(self):
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(
            dataset_id="test/ds",
            reproduction_cost={"total": 1000},
            human_percentage=70.0,
        )
        from datarecipe.core.project_layout import OUTPUT_SUBDIRS
        self.core._generate_ai_pipeline(
            output_mgr, result, "test/ds", "evaluation",
            False, False, OUTPUT_SUBDIRS,
        )
        path = output_mgr.get_path("ai_agent", "pipeline.yaml")
        self.assertTrue(os.path.exists(path))
        with open(path) as f:
            content = f.read()
        self.assertIn("test/ds", content)
        self.assertIn("phases:", content)

    def test_generate_ai_agent_readme(self):
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(dataset_id="test/ds")
        from datarecipe.core.project_layout import OUTPUT_SUBDIRS
        self.core._generate_ai_agent_readme(
            output_mgr, result, "test/ds", "evaluation", OUTPUT_SUBDIRS,
        )
        path = output_mgr.get_path("ai_agent", "README.md")
        self.assertTrue(os.path.exists(path))
        with open(path) as f:
            content = f.read()
        self.assertIn("test/ds", content)
        self.assertIn("agent_context.json", content)

    def test_generate_ai_agent_layer_full(self):
        """Test the orchestrator _generate_ai_agent_layer method."""
        output_mgr = self._make_output_mgr()
        result = AnalysisResult(
            dataset_id="test/ds",
            reproduction_cost={"total": 1000, "human": 700, "api": 300},
            human_percentage=70.0,
        )
        self.core._generate_ai_agent_layer(
            output_mgr=output_mgr,
            result=result,
            dataset_id="test/ds",
            dataset_type="evaluation",
            sample_count=100,
            actual_size=1000,
            allocation=StubAllocation(),
            complexity_metrics=StubComplexityMetrics(),
            rubrics_result=StubRubricsResult(),
            prompt_library=StubPromptLibrary(),
            llm_analysis=None,
            is_preference_dataset=False,
            is_swe_dataset=False,
        )
        # All 5 files should be generated
        output_mgr.get_path("ai_agent", "")
        expected_files = [
            "agent_context.json",
            "workflow_state.json",
            "reasoning_traces.json",
            "pipeline.yaml",
            "README.md",
        ]
        for fname in expected_files:
            path = output_mgr.get_path("ai_agent", fname)
            self.assertTrue(os.path.exists(path), f"{fname} should exist")


# ==================== Full analyze() Orchestrator Tests ====================


def _build_mock_dataset(items):
    """Build a mock iterable dataset from a list of dicts."""
    class MockDataset:
        def __iter__(self):
            return iter(items)
    return MockDataset()


class TestAnalyzeOrchestrator(unittest.TestCase):
    """Test the full analyze() method with comprehensive mocking."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        self.core = DeepAnalyzerCore(output_dir=self.tmpdir)

    def tearDown(self):
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def test_analyze_dataset_not_found_train_split(self):
        """Test that analyze handles dataset load failure on all splits."""
        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=MagicMock(side_effect=ValueError("Cannot load")),
                get_dataset_config_names=MagicMock(side_effect=Exception("Not found")),
            ),
        }):
            result = self.core.analyze("nonexistent/dataset")
            self.assertFalse(result.success)
            self.assertIn("Cannot find available split", result.error)

    def test_analyze_dataset_load_raises_runtime(self):
        """Test that RuntimeError is caught and surfaced."""
        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=MagicMock(side_effect=RuntimeError("Network error")),
                get_dataset_config_names=MagicMock(return_value=[]),
            ),
        }):
            result = self.core.analyze("bad/dataset", split="train")
            self.assertFalse(result.success)
            self.assertIn("Network error", result.error)


class TestAnalyzeOrchestratorSimplified(unittest.TestCase):
    """Simplified orchestrator tests that mock at a higher level."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()

    def tearDown(self):
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def test_analyze_catches_top_level_exception(self):
        """If any exception occurs, result.success=False and error is set."""
        core = DeepAnalyzerCore(output_dir=self.tmpdir)
        # Patch load_dataset to raise
        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=MagicMock(side_effect=RuntimeError("Connection failed")),
                get_dataset_config_names=MagicMock(side_effect=Exception("no configs")),
            ),
        }):
            result = core.analyze("bad/dataset", split="train")

        self.assertFalse(result.success)
        self.assertIn("Connection failed", result.error)

    def test_analyze_split_auto_detect_fallback(self):
        """When train split fails, should try test, validation, dev."""
        core = DeepAnalyzerCore(output_dir=self.tmpdir)
        call_count = [0]

        def mock_load(ds_id, **kwargs):
            call_count[0] += 1
            split = kwargs.get("split")
            if split == "train":
                raise ValueError("No train split")
            if split == "test":
                raise ValueError("No test split")
            if split == "validation":
                # Return a mock dataset
                return iter([{"text": "hello"}])
            raise ValueError("No such split")

        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=mock_load,
                get_dataset_config_names=MagicMock(return_value=[]),
            ),
        }):
            result = core.analyze("test/dataset")

        # Should have tried train, test, validation
        # The analyze will fail at some later point since we didn't mock everything,
        # but the split detection should have worked
        self.assertIsInstance(result, AnalysisResult)

    def test_analyze_no_available_split_raises(self):
        """When no split is available, should fail gracefully."""
        core = DeepAnalyzerCore(output_dir=self.tmpdir)

        def mock_load(ds_id, **kwargs):
            raise ValueError("No such split")

        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=mock_load,
                get_dataset_config_names=MagicMock(return_value=[]),
            ),
        }):
            result = core.analyze("test/dataset")

        self.assertFalse(result.success)
        self.assertIn("Cannot find available split", result.error)

    def test_analyze_with_explicit_split(self):
        """When split is specified, should use it directly."""
        core = DeepAnalyzerCore(output_dir=self.tmpdir)

        def mock_load(ds_id, **kwargs):
            split = kwargs.get("split")
            if split == "custom":
                return iter([{"text": "hello"}])
            raise ValueError(f"No split: {split}")

        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=mock_load,
                get_dataset_config_names=MagicMock(return_value=[]),
            ),
        }):
            result = core.analyze("test/dataset", split="custom")

        # May fail later but should not fail on split detection
        self.assertIsInstance(result, AnalysisResult)

    def test_analyze_config_detection_default(self):
        """Should prefer 'default' config when available."""
        core = DeepAnalyzerCore(output_dir=self.tmpdir)
        captured_kwargs = {}

        def mock_load(ds_id, **kwargs):
            captured_kwargs.update(kwargs)
            return iter([{"text": "hello"}])

        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=mock_load,
                get_dataset_config_names=MagicMock(return_value=["other", "default"]),
            ),
        }):
            core.analyze("test/dataset", split="train")

        self.assertEqual(captured_kwargs.get("name"), "default")

    def test_analyze_config_detection_first(self):
        """When no 'default' config, should use the first one."""
        core = DeepAnalyzerCore(output_dir=self.tmpdir)
        captured_kwargs = {}

        def mock_load(ds_id, **kwargs):
            captured_kwargs.update(kwargs)
            return iter([{"text": "hello"}])

        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=mock_load,
                get_dataset_config_names=MagicMock(return_value=["first_config", "second_config"]),
            ),
        }):
            core.analyze("test/dataset", split="train")

        self.assertEqual(captured_kwargs.get("name"), "first_config")


class TestAnalyzeFullIntegration(unittest.TestCase):
    """Full integration test of analyze() with all imports mocked at module level."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        # Create all needed subdirectories
        for subdir in [
            "01_决策参考", "02_项目管理", "03_标注规范", "04_复刻指南",
            "05_成本分析", "06_原始数据", "08_AI_Agent",
        ]:
            os.makedirs(os.path.join(self.tmpdir, "test_dataset", subdir), exist_ok=True)

    def tearDown(self):
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def _create_items(self, n=3, include_rubrics=True, include_messages=True):
        items = []
        for i in range(n):
            item = {"text": f"Sample text {i}" * 20}
            if include_rubrics:
                item["rubrics"] = ["Be accurate", "Be helpful"]
            if include_messages:
                item["messages"] = [
                    {"role": "system", "content": "You are a helpful assistant. " * 5},
                    {"role": "user", "content": "Tell me about AI. " * 10},
                    {"role": "assistant", "content": "AI is fascinating."},
                ]
            item["context"] = "A long context field that should be detected as context for analysis." * 2
            items.append(item)
        return items

    @patch("datarecipe.cache.AnalysisCache", MagicMock())
    @patch("datarecipe.knowledge.KnowledgeBase", MagicMock())
    def test_full_analyze_with_rubrics_and_messages(self):
        """Full analysis with items containing rubrics and messages."""
        items = self._create_items(3)
        mock_ds = _build_mock_dataset(items)
        core = DeepAnalyzerCore(output_dir=self.tmpdir)

        # Create comprehensive mocks for all dependencies
        mock_allocation = StubAllocation()
        mock_rubrics_result = StubRubricsResult()
        mock_prompt_lib = StubPromptLibrary()
        mock_strategy = StubStrategyResult()

        mock_splitter_instance = MagicMock()
        mock_splitter_instance.analyze.return_value = mock_allocation
        mock_splitter_instance.to_dict.return_value = {"tasks": []}

        mock_rubrics_instance = MagicMock()
        mock_rubrics_instance.analyze.return_value = mock_rubrics_result
        mock_rubrics_instance.to_dict.return_value = {}
        mock_rubrics_instance.to_yaml_templates.return_value = "# yaml"
        mock_rubrics_instance.to_markdown_templates.return_value = "# md"

        mock_extractor_instance = MagicMock()
        mock_extractor_instance.extract.return_value = mock_prompt_lib
        mock_extractor_instance.to_dict.return_value = {}

        mock_detector_instance = MagicMock()
        mock_detector_instance.analyze.return_value = mock_strategy
        mock_detector_instance.to_dict.return_value = {}

        mock_radar = MagicMock()
        mock_radar.create_summary.return_value = {}
        mock_radar.save_summary.return_value = "path"

        # Mock all the classes/imports used inside analyze()
        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=MagicMock(return_value=mock_ds),
                get_dataset_config_names=MagicMock(return_value=["default"]),
            ),
            "datarecipe.analyzers": MagicMock(
                ContextStrategyDetector=MagicMock(return_value=mock_detector_instance),
            ),
            "datarecipe.extractors": MagicMock(
                PromptExtractor=MagicMock(return_value=mock_extractor_instance),
                RubricsAnalyzer=MagicMock(return_value=mock_rubrics_instance),
            ),
            "datarecipe.generators": MagicMock(
                HumanMachineSplitter=MagicMock(return_value=mock_splitter_instance),
                TaskType=MagicMock(
                    CONTEXT_CREATION="cc",
                    TASK_DESIGN="td",
                    RUBRICS_WRITING="rw",
                    DATA_GENERATION="dg",
                    QUALITY_REVIEW="qr",
                ),
            ),
            "datarecipe.integrations.radar": MagicMock(
                RadarIntegration=mock_radar,
            ),
            "datarecipe.cost": MagicMock(
                PreciseCostCalculator=MagicMock(side_effect=Exception("skip")),
                ComplexityAnalyzer=MagicMock(side_effect=Exception("skip")),
                CostCalibrator=MagicMock(side_effect=Exception("skip")),
                PhasedCostModel=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.annotation_spec": MagicMock(
                AnnotationSpecGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.milestone_plan": MagicMock(
                MilestonePlanGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.executive_summary": MagicMock(
                ExecutiveSummaryGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.industry_benchmark": MagicMock(
                IndustryBenchmarkGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.llm_enhancer": MagicMock(
                LLMEnhancer=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.cache": MagicMock(),
            "datarecipe.knowledge": MagicMock(),
        }):
            result = core.analyze("test/dataset", sample_size=3, split="train")

        self.assertTrue(result.success)
        self.assertEqual(result.dataset_id, "test/dataset")
        self.assertEqual(result.sample_count, 3)
        self.assertEqual(result.dataset_type, "evaluation")  # has rubrics
        self.assertEqual(result.rubric_patterns, 10)
        self.assertEqual(result.prompt_templates, 5)

    @patch("datarecipe.cache.AnalysisCache", MagicMock())
    @patch("datarecipe.knowledge.KnowledgeBase", MagicMock())
    def test_full_analyze_preference_dataset(self):
        """Full analysis detecting preference (RLHF) dataset."""
        items = [
            {
                "chosen": "\n\nHuman: Hello\n\nAssistant: Hi there, how can I help?",
                "rejected": "\n\nHuman: Hello\n\nAssistant: Go away.",
            }
            for _ in range(3)
        ]
        mock_ds = _build_mock_dataset(items)
        core = DeepAnalyzerCore(output_dir=self.tmpdir)

        mock_allocation = StubAllocation()
        mock_splitter_instance = MagicMock()
        mock_splitter_instance.analyze.return_value = mock_allocation
        mock_splitter_instance.to_dict.return_value = {"tasks": []}

        mock_radar = MagicMock()
        mock_radar.create_summary.return_value = {}
        mock_radar.save_summary.return_value = "path"

        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=MagicMock(return_value=mock_ds),
                get_dataset_config_names=MagicMock(return_value=[]),
            ),
            "datarecipe.analyzers": MagicMock(
                ContextStrategyDetector=MagicMock(return_value=MagicMock(
                    analyze=MagicMock(return_value=StubStrategyResult()),
                    to_dict=MagicMock(return_value={}),
                )),
            ),
            "datarecipe.extractors": MagicMock(
                PromptExtractor=MagicMock(return_value=MagicMock(
                    extract=MagicMock(return_value=StubPromptLibrary(unique_count=0)),
                    to_dict=MagicMock(return_value={}),
                )),
                RubricsAnalyzer=MagicMock(return_value=MagicMock()),
            ),
            "datarecipe.generators": MagicMock(
                HumanMachineSplitter=MagicMock(return_value=mock_splitter_instance),
                TaskType=MagicMock(
                    CONTEXT_CREATION="cc", TASK_DESIGN="td",
                    RUBRICS_WRITING="rw", DATA_GENERATION="dg",
                    QUALITY_REVIEW="qr",
                ),
            ),
            "datarecipe.integrations.radar": MagicMock(
                RadarIntegration=mock_radar,
            ),
            "datarecipe.cost": MagicMock(
                PreciseCostCalculator=MagicMock(side_effect=Exception("skip")),
                ComplexityAnalyzer=MagicMock(side_effect=Exception("skip")),
                CostCalibrator=MagicMock(side_effect=Exception("skip")),
                PhasedCostModel=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.annotation_spec": MagicMock(
                AnnotationSpecGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.milestone_plan": MagicMock(
                MilestonePlanGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.executive_summary": MagicMock(
                ExecutiveSummaryGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.industry_benchmark": MagicMock(
                IndustryBenchmarkGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.llm_enhancer": MagicMock(
                LLMEnhancer=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.cache": MagicMock(),
            "datarecipe.knowledge": MagicMock(),
        }):
            result = core.analyze("test/dataset", sample_size=3, split="train")

        self.assertTrue(result.success)
        self.assertEqual(result.dataset_type, "preference")

    @patch("datarecipe.cache.AnalysisCache", MagicMock())
    @patch("datarecipe.knowledge.KnowledgeBase", MagicMock())
    def test_full_analyze_swe_dataset(self):
        """Full analysis detecting SWE-bench dataset."""
        items = [
            {
                "repo": "django/django",
                "patch": "+added\n-removed",
                "problem_statement": "Fix a bug in the ORM layer",
                "repo_language": "Python",
            }
            for _ in range(3)
        ]
        mock_ds = _build_mock_dataset(items)
        core = DeepAnalyzerCore(output_dir=self.tmpdir)

        mock_allocation = StubAllocation()
        mock_splitter_instance = MagicMock()
        mock_splitter_instance.analyze.return_value = mock_allocation
        mock_splitter_instance.to_dict.return_value = {"tasks": []}

        mock_radar = MagicMock()
        mock_radar.create_summary.return_value = {}
        mock_radar.save_summary.return_value = "path"

        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=MagicMock(return_value=mock_ds),
                get_dataset_config_names=MagicMock(return_value=[]),
            ),
            "datarecipe.analyzers": MagicMock(
                ContextStrategyDetector=MagicMock(return_value=MagicMock(
                    analyze=MagicMock(return_value=StubStrategyResult()),
                    to_dict=MagicMock(return_value={}),
                )),
            ),
            "datarecipe.extractors": MagicMock(
                PromptExtractor=MagicMock(return_value=MagicMock(
                    extract=MagicMock(return_value=StubPromptLibrary(unique_count=0)),
                    to_dict=MagicMock(return_value={}),
                )),
                RubricsAnalyzer=MagicMock(return_value=MagicMock()),
            ),
            "datarecipe.generators": MagicMock(
                HumanMachineSplitter=MagicMock(return_value=mock_splitter_instance),
                TaskType=MagicMock(
                    CONTEXT_CREATION="cc", TASK_DESIGN="td",
                    RUBRICS_WRITING="rw", DATA_GENERATION="dg",
                    QUALITY_REVIEW="qr",
                ),
            ),
            "datarecipe.integrations.radar": MagicMock(
                RadarIntegration=mock_radar,
            ),
            "datarecipe.cost": MagicMock(
                PreciseCostCalculator=MagicMock(side_effect=Exception("skip")),
                ComplexityAnalyzer=MagicMock(side_effect=Exception("skip")),
                CostCalibrator=MagicMock(side_effect=Exception("skip")),
                PhasedCostModel=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.annotation_spec": MagicMock(
                AnnotationSpecGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.milestone_plan": MagicMock(
                MilestonePlanGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.executive_summary": MagicMock(
                ExecutiveSummaryGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.industry_benchmark": MagicMock(
                IndustryBenchmarkGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.llm_enhancer": MagicMock(
                LLMEnhancer=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.cache": MagicMock(),
            "datarecipe.knowledge": MagicMock(),
        }):
            result = core.analyze("test/dataset", sample_size=3, split="train")

        self.assertTrue(result.success)
        self.assertEqual(result.dataset_type, "swe_bench")


# ==================== Schema / Sample Collection Tests ====================


class TestSampleCollectionLogic(unittest.TestCase):
    """Test the sample collection logic within analyze() by examining specific behaviors."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()

    def tearDown(self):
        import shutil
        shutil.rmtree(self.tmpdir, ignore_errors=True)

    def test_schema_info_collection(self):
        """Test that schema info is collected from first 10 items."""
        items = [
            {"field_a": f"value_{i}", "field_b": i, "field_c": [1, 2, 3]}
            for i in range(15)
        ]
        mock_ds = _build_mock_dataset(items)
        core = DeepAnalyzerCore(output_dir=self.tmpdir)

        mock_allocation = StubAllocation()
        mock_splitter_instance = MagicMock()
        mock_splitter_instance.analyze.return_value = mock_allocation
        mock_splitter_instance.to_dict.return_value = {}

        mock_radar = MagicMock()
        mock_radar.create_summary.return_value = {}
        mock_radar.save_summary.return_value = "path"

        with patch.dict("sys.modules", {
            "datasets": MagicMock(
                load_dataset=MagicMock(return_value=mock_ds),
                get_dataset_config_names=MagicMock(return_value=[]),
            ),
            "datarecipe.analyzers": MagicMock(
                ContextStrategyDetector=MagicMock(return_value=MagicMock(
                    analyze=MagicMock(return_value=StubStrategyResult()),
                    to_dict=MagicMock(return_value={}),
                )),
            ),
            "datarecipe.extractors": MagicMock(
                PromptExtractor=MagicMock(return_value=MagicMock(
                    extract=MagicMock(return_value=StubPromptLibrary(unique_count=0)),
                    to_dict=MagicMock(return_value={}),
                )),
                RubricsAnalyzer=MagicMock(return_value=MagicMock()),
            ),
            "datarecipe.generators": MagicMock(
                HumanMachineSplitter=MagicMock(return_value=mock_splitter_instance),
                TaskType=MagicMock(
                    CONTEXT_CREATION="cc", TASK_DESIGN="td",
                    RUBRICS_WRITING="rw", DATA_GENERATION="dg",
                    QUALITY_REVIEW="qr",
                ),
            ),
            "datarecipe.integrations.radar": MagicMock(
                RadarIntegration=mock_radar,
            ),
            "datarecipe.cost": MagicMock(
                PreciseCostCalculator=MagicMock(side_effect=Exception("skip")),
                ComplexityAnalyzer=MagicMock(side_effect=Exception("skip")),
                CostCalibrator=MagicMock(side_effect=Exception("skip")),
                PhasedCostModel=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.annotation_spec": MagicMock(
                AnnotationSpecGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.milestone_plan": MagicMock(
                MilestonePlanGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.executive_summary": MagicMock(
                ExecutiveSummaryGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.industry_benchmark": MagicMock(
                IndustryBenchmarkGenerator=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.generators.llm_enhancer": MagicMock(
                LLMEnhancer=MagicMock(side_effect=Exception("skip")),
            ),
            "datarecipe.cache": MagicMock(),
            "datarecipe.knowledge": MagicMock(),
        }):
            result = core.analyze("test/dataset", sample_size=15, split="train")

        self.assertTrue(result.success)
        self.assertEqual(result.sample_count, 15)
        self.assertIn("field_a", result.fields)
        self.assertIn("field_b", result.fields)
        self.assertIn("field_c", result.fields)


class TestPreEnhancedContext(unittest.TestCase):
    """Test pre_enhanced_context (MCP two-step workflow)."""

    def test_pre_enhanced_context_stored(self):
        ctx = StubEnhancedContext()
        core = DeepAnalyzerCore(pre_enhanced_context=ctx)
        self.assertIs(core.pre_enhanced_context, ctx)

    def test_pre_enhanced_context_none_by_default(self):
        core = DeepAnalyzerCore()
        self.assertIsNone(core.pre_enhanced_context)


# ==================== Edge Cases and Error Handling ====================


class TestEdgeCases(unittest.TestCase):
    """Test edge cases in helper methods."""

    def setUp(self):
        self.core = DeepAnalyzerCore()

    def test_preference_pair_empty_conversation(self):
        """Preference pair with no conversation markers."""
        item = {
            "chosen": "Just plain text without Human/Assistant markers",
            "rejected": "Another plain text",
        }
        pairs, topics, patterns = [], {}, {
            "chosen_longer": 0, "rejected_longer": 0, "same_length": 0,
            "chosen_more_detailed": 0, "chosen_more_helpful": 0, "chosen_safer": 0,
        }
        self.core._analyze_preference_pair(item, pairs, topics, patterns)
        # Should still work, topic defaults to 'general'
        self.assertIn("general", topics)

    def test_swe_item_empty_patch(self):
        stats = {
            "repos": {}, "languages": {}, "issue_types": {},
            "issue_categories": {}, "patch_lines": [], "examples": [],
        }
        item = {
            "repo": "test/repo",
            "patch": "",
            "problem_statement": "A problem",
        }
        self.core._analyze_swe_item(item, stats)
        self.assertEqual(stats["patch_lines"], [0])

    def test_report_with_all_none_optionals(self):
        """Report generation with all optional params as None."""
        allocation = StubAllocation()
        report = self.core._generate_analysis_report(
            dataset_id="ds",
            sample_count=10,
            actual_size=100,
            rubrics_result=None,
            prompt_library=None,
            strategy_result=None,
            allocation=allocation,
            region="china",
            enhanced_context=None,
        )
        self.assertIn("ds", report)
        self.assertIn("DataRecipe", report)

    def test_analysis_result_error_state(self):
        r = AnalysisResult(dataset_id="err/ds", success=False, error="Something broke")
        self.assertFalse(r.success)
        self.assertEqual(r.error, "Something broke")
        d = r.to_dict()
        self.assertFalse(d["success"])
        self.assertEqual(d["error"], "Something broke")


class TestAnalysisResultWarnings(unittest.TestCase):
    """Test AnalysisResult warnings handling."""

    def test_warnings_accumulate(self):
        r = AnalysisResult(dataset_id="test")
        r.warnings.append("Warning 1")
        r.warnings.append("Warning 2")
        self.assertEqual(len(r.warnings), 2)
        d = r.to_dict()
        self.assertEqual(d["warnings"], ["Warning 1", "Warning 2"])

    def test_files_generated_accumulate(self):
        r = AnalysisResult(dataset_id="test")
        r.files_generated.append("file1.md")
        r.files_generated.append("file2.json")
        self.assertEqual(len(r.files_generated), 2)


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