from typing import (
    Any,
    cast,
)

from galaxy import model
from galaxy.managers.context import ProvidesHistoryContext
from galaxy.util.unittest import TestCase
from galaxy.workflow import extract

UNDEFINED_JOB = object()


class TestWorkflowExtractSummary(TestCase):
    def setUp(self):
        self.history = MockHistory()
        self.trans = MockTrans(self.history)

    def _summarize(self) -> tuple[dict[Any, Any], set[str]]:
        """Helper to call summarize with mock trans, isolating the type cast."""
        return extract.summarize(trans=cast(ProvidesHistoryContext, self.trans))

    def test_empty_history(self):
        job_dict, warnings = self._summarize()
        assert not warnings
        assert not job_dict

    def test_summarize_returns_name_and_dataset_list(self):
        # Create two jobs and three datasets, test they are groupped
        # by job correctly with correct output names.
        hda1 = MockHda()
        self.history.active_datasets.append(hda1)
        hda2 = MockHda(job=hda1.job, output_name="out2")
        self.history.active_datasets.append(hda2)
        hda3 = MockHda(output_name="out3")
        self.history.active_datasets.append(hda3)

        job_dict, warnings = self._summarize()
        assert len(job_dict) == 2
        assert not warnings
        assert job_dict[hda1.job] == [("out1", hda1), ("out2", hda2)]
        assert job_dict[hda3.job] == [("out3", hda3)]

    def test_finds_original_job_if_copied(self):
        hda = MockHda()
        derived_hda_1 = MockHda()
        derived_hda_1.copied_from_history_dataset_association = hda
        derived_hda_2 = MockHda()
        derived_hda_2.copied_from_history_dataset_association = derived_hda_1
        self.history.active_datasets.append(derived_hda_2)
        job_dict, warnings = self._summarize()
        assert not warnings
        assert len(job_dict) == 1
        assert job_dict[hda.job] == [("out1", derived_hda_2)]

    def test_fake_job_hda(self):
        """Fakes job if creating_job_associations is empty."""
        hda = MockHda(job=UNDEFINED_JOB)
        self.history.active_datasets.append(hda)
        job_dict, warnings = self._summarize()
        assert not warnings
        assert len(job_dict) == 1
        fake_job = next(iter(job_dict.keys()))
        assert fake_job.id.startswith("fake_")
        datasets = next(iter(job_dict.values()))
        assert datasets == [(None, hda)]

    def test_fake_job_hda_name_guess(self):
        hda_from_history = MockHda(job=UNDEFINED_JOB)
        hda_from_history.copied_from_history_dataset_association = MockHda(job=UNDEFINED_JOB)
        self.history.active_datasets.append(hda_from_history)
        job_dict, warnings = self._summarize()
        assert not warnings
        assert len(job_dict) == 1
        fake_job = next(iter(job_dict.keys()))
        assert "History" in fake_job.name
        self.history.active_datasets.remove(hda_from_history)

        hda_from_library = MockHda(job=UNDEFINED_JOB)
        hda_from_library.copied_from_library_dataset_dataset_association = MockHda(job=UNDEFINED_JOB)
        self.history.active_datasets.append(hda_from_library)
        job_dict, warnings = self._summarize()
        assert not warnings
        assert len(job_dict) == 1
        fake_job = next(iter(job_dict.keys()))
        assert "Library" in fake_job.name

    def test_fake_job_hdca(self):
        hdca = MockHdca()
        self.history.active_datasets.append(hdca)
        job_dict, warnings = self._summarize()
        assert not warnings
        assert len(job_dict) == 1
        fake_job = next(iter(job_dict.keys()))
        assert fake_job.id.startswith("fake_")
        assert fake_job.is_fake
        content_instances = next(iter(job_dict.values()))
        assert content_instances == [(None, hdca)]

    def test_implicit_map_job_hdca(self):
        creating_job = model.Job()
        hdca = MockHdca(implicit_output_name="out1", job=creating_job)
        self.history.active_datasets.append(hdca)
        job_dict, warnings = self._summarize()
        assert not warnings
        assert len(job_dict) == 1
        job = next(iter(job_dict.keys()))
        assert job is creating_job

    def test_warns_and_skips_datasets_if_not_finished(self):
        hda = MockHda(state="queued")
        self.history.active_datasets.append(hda)
        job_dict, warnings = self._summarize()
        assert warnings
        assert len(job_dict) == 0


class MockJobToOutputDatasetAssociation:
    job = None

    def __init__(self, name, dataset):
        self.name = name
        self.dataset = dataset


class MockHistory:
    def __init__(self):
        self.active_datasets = []

    @property
    def active_contents(self):
        return self.active_datasets

    @property
    def visible_contents(self):
        return self.active_contents


class MockTrans:
    def __init__(self, history):
        self.history = history

    def get_history(self):
        return self.history


class MockHda:
    def __init__(self, state="ok", output_name="out1", job=None):
        self.hid = 1
        self.id = 123
        self.state = state
        self.copied_from_history_dataset_association = None
        self.copied_from_library_dataset_dataset_association = None
        self.history_content_type = "dataset"
        if job is not UNDEFINED_JOB:
            if not job:
                job = model.Job()
            self.job = job
            assoc = MockJobToOutputDatasetAssociation(output_name, self)
            assoc.job = job
            self.creating_job_associations = [assoc]
        else:
            self.creating_job_associations = []


class MockHdca:
    def __init__(self, implicit_output_name=None, job=None, hid=1):
        self.id = 124
        self.copied_from_history_dataset_collection_association = None
        self.history_content_type = "dataset_collection"
        self.implicit_output_name = implicit_output_name
        self.hid = 1
        self.collection = model.DatasetCollection()
        self.creating_job_associations = []
        element = model.DatasetCollectionElement(
            collection=self.collection,
            element=model.HistoryDatasetAssociation(),
            element_index=0,
            element_identifier="moocow",
        )
        element.dataset_instance.dataset = model.Dataset()
        element.dataset_instance.dataset.state = "ok"
        creating = model.JobToOutputDatasetAssociation(
            implicit_output_name,
            element.dataset_instance,
        )
        creating.job = job
        element.dataset_instance.creating_job_associations = [
            creating,
        ]
