#!/usr/bin/env python3
"""Test suite for dlist formatters

Covers ASCII, Markdown, LaTeX, and Excel formatters including
basic tables, auto-width, pagination, multi-column, categorized
output, void grouping, and list field display.
"""

import pytest
from dlist import Dlist
from dlist.formatters import get_formatter
from dlist.helpers import cleanStr


# ─── Test data ───────────────────────────────────────────────

@pytest.fixture
def sample_data():
    return Dlist([
        {'id': 'E0001', 'name': 'Sintel',   'type': 'E', 'dir': '/'},
        {'id': 'E0002', 'name': 'arroyito', 'type': 'E', 'dir': '/flaw/'},
        {'id': 'E0003', 'name': 'oficina',  'type': 'A', 'dir': '/flaw/'},
        {'id': 'E0004', 'name': 'test_4',   'type': 'A', 'dir': '/test/'},
    ], id_='id')


@pytest.fixture
def tagged_data():
    return Dlist([
        {'id': '1', 'name': 'photo1', 'tags': ['photo', 'landscape']},
        {'id': '2', 'name': 'video1', 'tags': ['video', 'interview']},
        {'id': '3', 'name': 'photo2', 'tags': ['photo', 'portrait']},
    ], id_='id')


@pytest.fixture
def nested_data():
    return Dlist([
        {'id': 'A1', 'cat': {'disk': 'VIDEOS'}, 'file': 'test.pdf'},
        {'id': 'A2', 'cat': {'disk': 'extras'}, 'file': '00167.MTS', 'subdir': '/other/'},
        {'id': 'A3', 'cat': {'disk': 'extras'}, 'file': 'hash.pdf',  'subdir': '/docs/'},
        {'id': 'A4', 'cat': {'disk': 'extras'}, 'file': 'info.txt',  'subdir': '/other/'},
        {'id': 'A5', 'cat': {'disk': 'extras'}, 'file': 'loose.txt'},
    ], id_='id')


# ─── cleanStr helper ────────────────────────────────────────

class TestCleanStr:
    def test_string(self):
        assert cleanStr('hello') == 'hello'

    def test_list(self):
        assert cleanStr(['a', 'b', 'c']) == 'a, b, c'

    def test_dict(self):
        result = cleanStr({'k': 'v', 'k2': 'v2'})
        assert 'k: v' in result
        assert 'k2: v2' in result

    def test_none(self):
        assert cleanStr(None) == 'None'

    def test_number(self):
        assert cleanStr(42) == '42'


# ─── get_formatter ───────────────────────────────────────────

class TestGetFormatter:
    def test_known_formats(self):
        for fmt in ('ascii', 'md', 'latex', 'json'):
            f = get_formatter(fmt)
            assert f is not None

    def test_unknown_raises(self):
        with pytest.raises(ValueError):
            get_formatter('unknown_format')

    def test_case_insensitive(self):
        assert get_formatter('ASCII') is get_formatter('ascii')


# ─── ASCII formatter ────────────────────────────────────────

class TestASCII:
    def test_basic_table(self, sample_data):
        out = sample_data.write(format='ascii', keys=['id', 'name'], width=10)
        assert 'E0001' in out
        assert 'Sintel' in out
        assert '┌' in out  # box-drawing

    def test_auto_width(self, sample_data):
        out = sample_data.write(format='ascii', keys=['id', 'name'], width='auto')
        assert 'E0001' in out
        assert 'arroyito' in out

    def test_titles(self, sample_data):
        out = sample_data.write(format='ascii', keys=['id', 'name'],
                                width='auto', titles={'id': 'ID', 'name': 'Name'})
        assert 'ID' in out
        assert 'Name' in out

    def test_multi_column(self, sample_data):
        out = sample_data.write(format='ascii', keys=['id', 'name'],
                                width=10, nCols=2)
        # Two column groups → header row has two 'id' occurrences
        assert out.count('id') >= 2
        # Cross separator between column groups
        assert '┼' in out

    def test_pagination(self, sample_data):
        out = sample_data.write(format='ascii', keys=['id', 'name'],
                                width=10, page=2)
        # Should have repeated headers (two pages)
        assert out.count('id') >= 2

    def test_categorized(self, sample_data):
        out = sample_data.write(format='ascii', ctree=['type'],
                                keys=['id', 'name'], width='auto',
                                titles={'type': 'Type {}', 'id': 'ID', 'name': 'Name'})
        assert 'Type E' in out
        assert 'Type A' in out

    def test_categorized_two_levels(self, nested_data):
        out = nested_data.write(format='ascii', ctree=['cat__disk', 'subdir'],
                                keys=['id', 'file'], width='auto',
                                titles={'cat__disk': '{}', 'subdir': 'Dir {}',
                                        'id': 'ID', 'file': 'File'})
        assert 'VIDEOS' in out
        assert 'extras' in out
        assert 'Dir /other/' in out

    def test_remainder_first(self, nested_data):
        out = nested_data.write(format='ascii', ctree=['cat__disk', 'subdir'],
                                keys=['id', 'file'], width='auto',
                                titles={'cat__disk': '{}', 'subdir': 'Dir {}',
                                        'id': 'ID', 'file': 'File'})
        # loose.txt (no subdir) should appear before Dir /other/
        pos_loose = out.find('loose.txt')
        pos_other = out.find('Dir /other/')
        assert pos_loose < pos_other

    def test_void_grouping(self, sample_data):
        out = sample_data.write(format='ascii', ctree=['void', 'type'],
                                keys=['id', 'name'], width='auto',
                                titles={'void': 'All Files', 'type': 'Type {}',
                                        'id': 'ID', 'name': 'Name'})
        assert 'All Files' in out
        assert 'Type E' in out
        assert 'Type A' in out

    def test_list_field_display(self, tagged_data):
        out = tagged_data.write(format='ascii', keys=['id', 'name', 'tags'],
                                width='auto',
                                titles={'id': 'ID', 'name': 'Name', 'tags': 'Tags'})
        # Lists should be displayed as comma-separated
        assert 'photo, landscape' in out

    def test_categorized_by_list_field(self, tagged_data):
        out = tagged_data.write(format='ascii', ctree=['tags'],
                                keys=['id', 'name'], width='auto',
                                titles={'tags': '{}', 'id': 'ID', 'name': 'Name'})
        # Items appear under each tag they belong to
        assert 'photo' in out
        assert 'video' in out

    def test_blank_placeholder(self, sample_data):
        # Add item without 'name'
        d = sample_data + Dlist([{'id': 'E0005', 'type': 'X'}], id_='id')
        out = d.write(format='ascii', keys=['id', 'name'], width=10)
        assert '---' in out  # default blank


# ─── Markdown formatter ─────────────────────────────────────

class TestMarkdown:
    def test_basic_table(self, sample_data):
        out = sample_data.write(format='md', keys=['id', 'name'], width='auto')
        assert '|' in out
        assert 'E0001' in out
        assert ':---' in out  # alignment row

    def test_titles(self, sample_data):
        out = sample_data.write(format='md', keys=['id', 'name'],
                                width='auto', titles={'id': 'ID', 'name': 'Name'})
        assert '|ID' in out or '| ID' in out

    def test_categorized_headings(self, sample_data):
        out = sample_data.write(format='md', ctree=['type'],
                                keys=['id', 'name'], width='auto',
                                titles={'type': 'Type {}', 'id': 'ID', 'name': 'Name'})
        assert '# Type E' in out
        assert '# Type A' in out

    def test_pagination_separator(self, sample_data):
        out = sample_data.write(format='md', keys=['id', 'name'],
                                width='auto', page=2)
        assert '&nbsp;' in out  # page separator

    def test_list_field_display(self, tagged_data):
        out = tagged_data.write(format='md', keys=['id', 'tags'], width='auto')
        assert 'photo, landscape' in out


# ─── LaTeX formatter ────────────────────────────────────────

class TestLaTeX:
    def test_basic_table(self, sample_data):
        out = sample_data.write(format='latex', keys=['id', 'name'], width='auto')
        assert '\\begin{tabular}' in out
        assert '\\end{tabular}' in out
        assert 'E0001' in out

    def test_titles(self, sample_data):
        out = sample_data.write(format='latex', keys=['id', 'name'],
                                width='auto', titles={'id': 'ID', 'name': 'Name'})
        assert 'ID' in out
        assert 'Name' in out

    def test_hline(self, sample_data):
        out = sample_data.write(format='latex', keys=['id', 'name'], width='auto')
        assert '\\hline' in out

    def test_categorized(self, sample_data):
        out = sample_data.write(format='latex', ctree=['type'],
                                keys=['id', 'name'], width='auto',
                                titles={'type': 'Type {}', 'id': 'ID', 'name': 'Name'})
        assert 'Type E' in out
        assert 'Type A' in out

    def test_ampersand_separator(self, sample_data):
        out = sample_data.write(format='latex', keys=['id', 'name'], width='auto')
        assert '&' in out  # LaTeX column separator

    def test_list_field_display(self, tagged_data):
        out = tagged_data.write(format='latex', keys=['id', 'tags'], width='auto')
        assert 'photo' in out


# ─── Excel formatter ────────────────────────────────────────

class TestExcel:
    @pytest.fixture
    def excel_available(self):
        try:
            import openpyxl
            return True
        except ImportError:
            pytest.skip("openpyxl not installed")

    def test_basic_workbook(self, sample_data, excel_available, tmp_path):
        from openpyxl import load_workbook
        path = tmp_path / 'test.xlsx'
        sample_data.write(str(path), format='excel',
                          keys=['id', 'name'],
                          titles={'id': 'ID', 'name': 'Name'})
        wb = load_workbook(path)
        ws = wb.active
        assert ws.cell(1, 1).value == 'ID'
        assert ws.cell(1, 2).value == 'Name'
        assert ws.cell(2, 1).value == 'E0001'

    def test_categorized_tabs(self, sample_data, excel_available, tmp_path):
        from openpyxl import load_workbook
        path = tmp_path / 'cat.xlsx'
        sample_data.write(str(path), format='excel',
                          ctree=['type'], keys=['id', 'name'],
                          titles={'type': 'Type {}', 'id': 'ID', 'name': 'Name'})
        wb = load_workbook(path)
        names = wb.sheetnames
        assert any('E' in n for n in names)
        assert any('A' in n for n in names)

    def test_subcategorized(self, nested_data, excel_available, tmp_path):
        from openpyxl import load_workbook
        path = tmp_path / 'subcat.xlsx'
        nested_data.write(str(path), format='excel',
                          ctree=['cat__disk', 'subdir'],
                          keys=['id', 'file'],
                          titles={'cat__disk': '{}', 'subdir': 'Dir {}',
                                  'id': 'ID', 'file': 'File'})
        wb = load_workbook(path)
        names = wb.sheetnames
        assert any('VIDEOS' in n for n in names)
        assert any('extras' in n for n in names)

    def test_void_grouping(self, sample_data, excel_available, tmp_path):
        from openpyxl import load_workbook
        path = tmp_path / 'void.xlsx'
        sample_data.write(str(path), format='excel',
                          ctree=['void', 'type'],
                          keys=['id', 'name'],
                          titles={'void': 'All Files', 'type': 'Type {}',
                                  'id': 'ID', 'name': 'Name'})
        wb = load_workbook(path)
        names = wb.sheetnames
        assert any('All Files' in n for n in names)

    def test_font_size_12(self, sample_data, excel_available, tmp_path):
        from openpyxl import load_workbook
        path = tmp_path / 'style.xlsx'
        sample_data.write(str(path), format='excel',
                          keys=['id', 'name'],
                          titles={'id': 'ID', 'name': 'Name'})
        wb = load_workbook(path)
        ws = wb.active
        assert ws.cell(1, 1).font.size == 12  # header
        assert ws.cell(2, 1).font.size == 12  # data

    def test_no_stripe(self, sample_data, excel_available, tmp_path):
        from openpyxl import load_workbook
        path = tmp_path / 'nostripe.xlsx'
        sample_data.write(str(path), format='excel',
                          keys=['id', 'name'],
                          titles={'id': 'ID', 'name': 'Name'})
        wb = load_workbook(path)
        ws = wb.active
        # Row 2 and row 3 should have same background (no stripe)
        fill2 = ws.cell(2, 1).fill
        fill3 = ws.cell(3, 1).fill
        assert fill2.fgColor == fill3.fgColor

    def test_list_field_in_table(self, tagged_data, excel_available, tmp_path):
        from openpyxl import load_workbook
        path = tmp_path / 'tags.xlsx'
        tagged_data.write(str(path), format='excel',
                          keys=['id', 'name', 'tags'],
                          titles={'id': 'ID', 'name': 'Name', 'tags': 'Tags'})
        wb = load_workbook(path)
        ws = wb.active
        # List values should be displayed as comma-separated
        tags_col = [ws.cell(r, 3).value for r in range(2, 5)]
        assert 'photo, landscape' in tags_col


# ─── write() integration ────────────────────────────────────

class TestWriteIntegration:
    def test_write_returns_string(self, sample_data):
        out = sample_data.write(format='ascii', keys=['id', 'name'], width=10)
        assert isinstance(out, str)

    def test_write_json_default(self, sample_data):
        out = sample_data.write()
        assert isinstance(out, str)
        assert 'E0001' in out

    def test_write_to_file(self, sample_data, tmp_path):
        path = tmp_path / 'out.txt'
        sample_data.write(str(path), format='ascii', keys=['id', 'name'], width=10)
        assert path.exists()
        content = path.read_text()
        assert 'E0001' in content

    def test_write_categorized_returns_string(self, sample_data):
        out = sample_data.write(format='ascii', ctree=['type'],
                                keys=['id', 'name'], width='auto',
                                titles={'type': '{}', 'id': 'ID', 'name': 'Name'})
        assert isinstance(out, str)
