"""Tests for the chaotic-staging scanner improvements in
``AutoImportWorker._scan_directory``.

Two related behaviors pinned here:

1. **Loose files grouped by album tag.** When the staging root has
   loose files from multiple different albums (user moved tracks out
   of their album folders + dumped them at root), each album's tracks
   get their own candidate via the embedded `album` tag. Pre-fix:
   everything bundled into one fake album, identifier picked the
   most-common tag, other albums' tracks left unmatched.

2. **Always recurse into non-disc subfolders.** Pre-fix the scanner
   would skip subfolders entirely when loose files existed at the
   same level. So a layout like::

       Staging/
         loose1.flac           ← processed
         Disc 1/               ← attached to loose
         Album Folder/         ← IGNORED

   would silently skip "Album Folder" because root had loose files.
   Post-fix: subfolders always recursed regardless of loose files.

Tests use temp directories with real FLAC files (mutagen-written)
so the scanner's tag reads exercise the real code path.
"""

from __future__ import annotations

import os
import struct
import tempfile

import pytest

from core.auto_import_worker import AutoImportWorker


def _write_flac(path: str, *, album: str = '', track: int = 0, disc: int = 1, title: str = 'Test'):
    """Write a real FLAC with the given tags. Same minimal-FLAC
    bootstrap pattern used elsewhere in the test suite."""
    from mutagen.flac import FLAC

    fLaC = b'fLaC'
    streaminfo = bytearray(34)
    streaminfo[0:2] = struct.pack('>H', 4096)
    streaminfo[2:4] = struct.pack('>H', 4096)
    streaminfo[10] = 0x0A
    streaminfo[12] = 0x70
    block_header = bytes([0x80, 0x00, 0x00, 0x22])
    with open(path, 'wb') as f:
        f.write(fLaC + block_header + bytes(streaminfo))

    audio = FLAC(path)
    if album:
        audio['ALBUM'] = album
    if track:
        audio['TRACKNUMBER'] = str(track)
    if disc:
        audio['DISCNUMBER'] = str(disc)
    if title:
        audio['TITLE'] = title
    audio.save()


@pytest.fixture
def worker():
    """Bare worker — `_scan_directory` doesn't need full deps."""
    return AutoImportWorker.__new__(AutoImportWorker)


# ---------------------------------------------------------------------------
# Loose-file grouping by album tag
# ---------------------------------------------------------------------------


def test_loose_files_from_multiple_albums_become_multiple_candidates(worker, tmp_path):
    """Two albums' worth of tracks at root → two candidates, not one
    bundle. Validates the chaotic-staging fix."""
    # Album A: 3 tracks
    for i in range(1, 4):
        _write_flac(
            str(tmp_path / f'A_{i}.flac'),
            album='Album A', track=i, title=f'A track {i}',
        )
    # Album B: 2 tracks
    for i in range(1, 3):
        _write_flac(
            str(tmp_path / f'B_{i}.flac'),
            album='Album B', track=i, title=f'B track {i}',
        )

    candidates = []
    worker._scan_directory(str(tmp_path), candidates, staging_root=str(tmp_path))

    assert len(candidates) == 2
    album_keys = sorted(
        len(c.audio_files) for c in candidates if not c.is_single
    )
    assert album_keys == [2, 3]   # one 3-track album + one 2-track album


def test_untagged_loose_files_become_individual_singles(worker, tmp_path):
    """Files with no album tag can't be grouped — each becomes its
    own single candidate."""
    _write_flac(str(tmp_path / 'orphan_a.flac'), album='', track=0)
    _write_flac(str(tmp_path / 'orphan_b.flac'), album='', track=0)

    candidates = []
    worker._scan_directory(str(tmp_path), candidates, staging_root=str(tmp_path))

    singles = [c for c in candidates if c.is_single]
    assert len(singles) == 2


def test_single_album_loose_files_still_one_candidate(worker, tmp_path):
    """Regression guard — when all loose files share an album, behavior
    matches the previous "bundle everything into one candidate" path.
    Single-album staging shouldn't fragment into per-track singles."""
    for i in range(1, 6):
        _write_flac(
            str(tmp_path / f'track_{i}.flac'),
            album='Single Album', track=i, title=f'Song {i}',
        )

    candidates = []
    worker._scan_directory(str(tmp_path), candidates, staging_root=str(tmp_path))

    album_candidates = [c for c in candidates if not c.is_single]
    assert len(album_candidates) == 1
    assert len(album_candidates[0].audio_files) == 5


# ---------------------------------------------------------------------------
# Always-recurse-into-subfolders
# ---------------------------------------------------------------------------


def test_subfolders_processed_even_when_root_has_loose_files(worker, tmp_path):
    """The original bug — root has loose files AND a non-disc
    subfolder. Pre-fix: subfolder ignored. Post-fix: subfolder
    recursed."""
    # Loose file at root
    _write_flac(
        str(tmp_path / 'loose.flac'),
        album='Loose Album', track=1, title='Loose Song',
    )

    # Subfolder with its own album
    sub = tmp_path / 'Other Album'
    sub.mkdir()
    for i in range(1, 4):
        _write_flac(
            str(sub / f't{i}.flac'),
            album='Other Album', track=i, title=f'Other {i}',
        )

    candidates = []
    worker._scan_directory(str(tmp_path), candidates, staging_root=str(tmp_path))

    # 1 candidate from loose + 1 candidate from subfolder = 2
    assert len(candidates) == 2
    paths = {c.path for c in candidates}
    assert any('Other Album' in p for p in paths), (
        f"Subfolder candidate missing — paths: {paths}. Pre-fix "
        f"behavior: scanner ignored the subfolder when root had "
        f"loose files."
    )


# ---------------------------------------------------------------------------
# Disc folder attachment to loose-file groups
# ---------------------------------------------------------------------------


def test_disc_folder_attaches_to_matching_loose_group(worker, tmp_path):
    """Loose Mr. Morale tracks at root + Disc 2 folder also tagged
    Mr. Morale → disc folder merges into the Mr. Morale loose
    candidate. Mirrors the user's typical multi-disc layout."""
    # Loose disc 1 tracks
    for i in range(1, 4):
        _write_flac(
            str(tmp_path / f'disc1_{i}.flac'),
            album='Mr. Morale', track=i, disc=1, title=f'D1 {i}',
        )

    # Disc 2 folder, same album
    disc2 = tmp_path / 'Disc 2'
    disc2.mkdir()
    for i in range(1, 4):
        _write_flac(
            str(disc2 / f'd2_{i}.flac'),
            album='Mr. Morale', track=i, disc=2, title=f'D2 {i}',
        )

    candidates = []
    worker._scan_directory(str(tmp_path), candidates, staging_root=str(tmp_path))

    assert len(candidates) == 1
    candidate = candidates[0]
    # All 6 files (3 loose + 3 disc 2) merged into one candidate
    assert len(candidate.audio_files) == 6
    # Disc structure carries both disc 0 (loose) + disc 2
    assert 0 in candidate.disc_structure
    assert 2 in candidate.disc_structure


def test_disc_folder_with_no_matching_loose_group_becomes_standalone(worker, tmp_path):
    """Loose Album A tracks at root + Disc 2 folder tagged Album B →
    disc folder doesn't merge into A; becomes its own candidate."""
    _write_flac(
        str(tmp_path / 'a1.flac'),
        album='Album A', track=1, title='A1',
    )
    _write_flac(
        str(tmp_path / 'a2.flac'),
        album='Album A', track=2, title='A2',
    )

    disc2 = tmp_path / 'Disc 2'
    disc2.mkdir()
    _write_flac(
        str(disc2 / 'b1.flac'),
        album='Album B', track=1, disc=2, title='B1',
    )

    candidates = []
    worker._scan_directory(str(tmp_path), candidates, staging_root=str(tmp_path))

    # 1 candidate for Album A loose + 1 candidate for the standalone
    # disc folder = 2 total
    assert len(candidates) == 2
    a_candidate = next(c for c in candidates if len(c.audio_files) == 2)
    standalone = next(c for c in candidates if c is not a_candidate)
    assert len(a_candidate.audio_files) == 2  # only Album A loose, no disc
    assert len(standalone.audio_files) == 1   # Album B disc 2 alone


# ---------------------------------------------------------------------------
# Disc-only directory (regression guard)
# ---------------------------------------------------------------------------


def test_sibling_candidates_have_unique_folder_hashes(worker, tmp_path):
    """Pin the dedup-collision bug: when loose files at one level
    produce multiple candidates (one per album group), each must have
    a unique folder_hash. The runtime's `_processing_hashes` set + the
    DB's `auto_import_history.folder_hash` index both key on this —
    if siblings collide, only the first one gets processed and the
    rest silently skip as "still processing from previous cycle."

    Reported on 2026-05-09 — multi-album loose root produced 3
    candidates with the same path. Path-keyed dedup treated them as
    duplicates and skipped two of three. Fixed by switching dedup to
    folder_hash + ensuring each candidate's hash is unique."""
    for i in range(1, 4):
        _write_flac(
            str(tmp_path / f'A_{i}.flac'),
            album='Album A', track=i, title=f'A {i}',
        )
    for i in range(1, 4):
        _write_flac(
            str(tmp_path / f'B_{i}.flac'),
            album='Album B', track=i, title=f'B {i}',
        )

    candidates = []
    worker._scan_directory(str(tmp_path), candidates, staging_root=str(tmp_path))

    hashes = [c.folder_hash for c in candidates]
    assert len(hashes) == len(set(hashes)), (
        f"Sibling candidates collided on folder_hash: {hashes}. "
        f"Path-keyed dedup would silently skip duplicates."
    )


def test_disc_only_directory_still_works(worker, tmp_path):
    """No loose files, only Disc 1/Disc 2 subfolders → treat parent
    directory as the album candidate. Pre-existing behavior preserved."""
    for disc_num in (1, 2):
        disc_dir = tmp_path / f'Disc {disc_num}'
        disc_dir.mkdir()
        for i in range(1, 4):
            _write_flac(
                str(disc_dir / f'd{disc_num}_t{i}.flac'),
                album='Disc Only Album', track=i, disc=disc_num,
                title=f'D{disc_num}T{i}',
            )

    candidates = []
    worker._scan_directory(str(tmp_path), candidates, staging_root=str(tmp_path))

    assert len(candidates) == 1
    assert len(candidates[0].audio_files) == 6
    assert candidates[0].disc_structure == {
        1: candidates[0].disc_structure[1],
        2: candidates[0].disc_structure[2],
    }
