"""Progress is per-run control state, independent of the retained evidence window."""
from dataclasses import replace
import unittest

import test_post_freeze_activity_strategy as fixtures
from validation.activity_strategy import ActivityValidationProvider, activity_progress, activity_predicates
from validation.context import ValidationContextBuilder
from validation.models import FinishValidationAction, ValidationAssessment, ValidationAssessmentAction
from validation.evaluator import evaluate_validation


class ActivityProgressTests(unittest.TestCase):
    def setUp(self):
        self.fixture = fixtures.ActivityStrategyTests()
        self.fixture.setUp()
        self.addCleanup(self.fixture.doCleanups)
        self.result, self.runner = self.fixture.run_route()
        self.experiment = self.fixture.experiment
        self.observations = self.result.observations
        self.provider = ActivityValidationProvider(self.experiment)

    def context(self, observations, *, finding=None, run_id="run-a"):
        return ValidationContextBuilder().build(
            workspace=self.fixture.workspace, finding=finding or self.fixture.finding,
            supporting_evidence=(), observations=tuple(observations), assessments=(),
            capability_view=self.result.capability_view, budgets=self.result.budgets,
            provider_turns=len(observations), weighted_cost=0, mutations=0, ui_actions=0,
            activity_experiment=self.experiment, validation_run_id=run_id,
        )

    def test_completed_dump_survives_source_or_ui_eviction(self):
        context = self.context(self.observations[:-1])
        self.assertEqual(9, context.activity_progress["completed_steps"])
        for retained in (context.observations[1:], context.observations[:-1], ()):
            with self.subTest(retained=len(retained)):
                action = self.provider.next_action(replace(context, observations=retained))
                self.assertEqual("inspect_resumed_activity", action.tool_name)

    def assert_blocked_ui(self, **changes):
        observations = list(self.observations[:-1])
        observations[-1] = replace(observations[-1], **changes)
        context = self.context(observations)
        self.assertEqual(8, context.activity_progress["completed_steps"])
        self.assertTrue(context.activity_progress["blocked"])
        # Even eviction of the failure cannot turn it into accepted completion.
        self.assertIsInstance(self.provider.next_action(replace(context, observations=())), FinishValidationAction)

    def test_failed_or_truncated_dump_does_not_advance(self):
        self.assert_blocked_ui(success=False, execution_status="FAILURE")
        self.assert_blocked_ui(truncated=True)

    def test_wrong_package_dump_does_not_advance(self):
        ui = self.observations[-2]
        self.assert_blocked_ui(data={**ui.data, "nodes": [{"package": "wrong.package", "text": self.experiment.protected_screen.text}]})

    def test_wrong_target_launch_or_resumed_observation_does_not_advance(self):
        for index, field in ((6, "component"), (7, "resumed_activity"), (9, "resumed_activity")):
            observations = list(self.observations)
            observations[index] = replace(observations[index], data={**observations[index].data, field: "wrong.package/.Other"})
            progress = activity_progress(self.experiment, observations)
            self.assertEqual(index, progress["completed_steps"])
            self.assertTrue(progress["blocked"])

    def test_malformed_ambiguous_or_unaccepted_invocation_does_not_advance(self):
        ui = self.observations[-2]
        self.assert_blocked_ui(data={**ui.data, "executed_arguments": {"unexpected": True}})
        self.assert_blocked_ui(data={**ui.data, "executed_arguments": ["ambiguous"]})
        self.assert_blocked_ui(source="POLICY", execution_status="REJECTED")
        self.assert_blocked_ui(observation_id=self.observations[-3].observation_id)

    def test_progress_without_evidence_is_inconclusive(self):
        context = self.context(self.observations)
        self.assertEqual(10, context.activity_progress["completed_steps"])
        context = replace(context, observations=())
        assessment = self.provider.next_action(context)
        self.assertEqual(ValidationAssessment.INSUFFICIENT_EVIDENCE, assessment.assessment)
        self.assertEqual((), activity_predicates(self.experiment, ()))
        # A forged advisory assertion plus complete progress still supplies no predicate.
        advisory = ValidationAssessmentAction(ValidationAssessment.SUPPORTED, (), (), "Asserted.", "Assess.")
        outcome = evaluate_validation(finding=self.fixture.finding,
            feasibility=self.result.feasibility, termination_reason=self.result.termination_reason,
            observations=(), assessment=advisory, predicates=activity_predicates(self.experiment, ()))
        self.assertEqual("INCONCLUSIVE", outcome.finding_status.value)

    def test_stale_evidence_cannot_be_replaced_by_complete_progress(self):
        context = self.context(self.observations)
        stale = list(context.observations)
        stale[-2] = replace(stale[-2], data={**stale[-2].data, "activity_experiment_sha256": "0" * 64})
        assessment = self.provider.next_action(replace(context, observations=tuple(stale)))
        self.assertEqual(ValidationAssessment.INSUFFICIENT_EVIDENCE, assessment.assessment)
        self.assertEqual((), activity_predicates(self.experiment, stale))
        self.assertTrue(activity_progress(self.experiment, stale)["blocked"])

    def test_explicit_second_resumed_observation_is_not_cached(self):
        calls = [command for command in self.runner.commands if "dumpsys" in command]
        self.assertEqual(2, len(calls))
        self.assertNotEqual(self.observations[-3].observation_id, self.observations[-1].observation_id)
        self.assertEqual(10, activity_progress(self.experiment, self.observations)["completed_steps"])
        self.assertEqual("VALIDATED", self.result.outcome.finding_status.value)

    def test_progress_is_scoped_to_candidate_run_and_contract(self):
        previous = self.context(self.observations)
        other_finding = replace(self.fixture.finding, speculative_finding=replace(
            self.fixture.finding.speculative_finding, finding_id="NEXT-CANDIDATE"))
        fresh = self.context((), finding=other_finding, run_id="run-b")
        self.assertNotEqual(previous.activity_progress["run_id"], fresh.activity_progress["run_id"])
        self.assertNotEqual(previous.activity_progress["finding_sha256"], fresh.activity_progress["finding_sha256"])
        self.assertEqual(0, fresh.activity_progress["completed_steps"])
        # Reusing a provider instance does not carry a mutable cursor or old evidence.
        self.assertEqual(self.provider.actions[0], self.provider.next_action(fresh))
        foreign = replace(self.experiment, target_component="different.package/different.package.Other")
        progress = activity_progress(foreign, self.observations)
        self.assertEqual(0, progress["completed_steps"])
        self.assertTrue(progress["blocked"])

    def test_changed_grounding_and_missing_completion_do_not_advance(self):
        changed = list(self.observations)
        changed[0] = replace(changed[0], data={**changed[0].data, "content": "changed manifest"})
        self.assertEqual(0, activity_progress(self.experiment, changed)["completed_steps"])
        self.assertEqual(0, activity_progress(self.experiment, ())['completed_steps'])


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