#!/usr/bin/env python
# Copyright (C) 2013  Johannes Dewender
# Copyright (C) 2017  Frederik “Freso” S. Olesen
# Copyright (C) 2025, 2026  Philipp Wolfer
# This test is free. You can redistribute and/or modify it at will.

import math
import os
import unittest
from collections.abc import Sequence
from dataclasses import dataclass

import discid
import discid.util


@dataclass
class DiscTestData:
    name: str
    first: int
    last: int
    sectors: int
    offsets: Sequence[int]
    id: str
    freedb: str


test_discs = [
    DiscTestData(
        name="Guano Apes - Don't give Me Names, without last data track",
        first=1,
        last=15,
        sectors=258725,
        offsets=[
            150,
            17510,
            33275,
            45910,
            57805,
            78310,
            94650,
            109580,
            132010,
            149160,
            165115,
            177710,
            203325,
            215555,
            235590,
        ],
        id="TqvKjMu7dMliSfmVEBtrL7sBSno-",
        freedb="b60d770f",
    ),
    DiscTestData(
        name="Lunar - There Is No 1, first track is 2",
        first=2,
        last=11,
        sectors=225781,
        offsets=(
            150,
            11512,
            34143,
            50747,
            63640,
            98491,
            123534,
            174410,
            195438,
            201127,
        ),
        id="6RDuz0d7.M5SVMLe1z4DP0yaEC8-",
        freedb="840bc20b",
    ),
    DiscTestData(
        name="Minimal",
        first=1,
        last=1,
        sectors=44942,
        offsets=[150],
        id="ANJa4DGYN_ktpzOwvVPtcjwP7mE-",
        freedb="02025501",
    ),
    DiscTestData(
        name="Korn - See You on the Other Side, with pre-gap audio track",
        first=1,
        last=14,
        sectors=275749,
        offsets=[
            5475,
            19645,
            34416,
            51655,
            68900,
            90015,
            111090,
            130510,
            158652,
            173635,
            189015,
            208122,
            224413,
            252866,
        ],
        id="CnkXRItZOUxex7JwyWmHfdbFdqE-",
        freedb="be0e130e",
    ),
]


class TestModulePrivate(unittest.TestCase):
    # lots of encoding tests
    # not part of the actual API, but used internally to handle both bytes and str
    def test_encode(self):
        self.assertTrue(type(discid.util._encode("test")) is bytes)
        self.assertEqual(discid.util._encode("test"), b"test")
        self.assertEqual(discid.util._encode(b"test"), b"test")
        with self.assertRaises(TypeError):
            discid.util._encode(42)  # type: ignore

    def test_decode(self):
        self.assertTrue(type(discid.util._decode(b"test")) is str)
        self.assertEqual(discid.util._decode(b"test"), "test")
        self.assertEqual(discid.util._decode("test"), "test")
        with self.assertRaises(TypeError):
            discid.util._encode(42)  # type: ignore

    def test_encoding(self):
        string = "test"
        self.assertEqual(discid.util._decode(discid.util._encode(string)), string)
        bytestring = b"test"
        self.assertEqual(
            discid.util._encode(discid.util._decode(bytestring)), bytestring
        )

    def test_sectors_to_seconds(self):
        self.assertEqual(0, discid.util._sectors_to_seconds(0))
        self.assertEqual(
            1, discid.util._sectors_to_seconds(discid.util.SECTORS_PER_SECOND)
        )
        self.assertEqual(
            1, discid.util._sectors_to_seconds(discid.util.SECTORS_PER_SECOND / 2)
        )
        self.assertEqual(
            0,
            discid.util._sectors_to_seconds((discid.util.SECTORS_PER_SECOND / 2) - 0.1),
        )
        self.assertIsInstance(discid.util._sectors_to_seconds(90.5), int)


class TestModule(unittest.TestCase):
    def test_version_string(self):
        version_string = discid.LIBDISCID_VERSION_STRING
        self.assertTrue(version_string is not None, "No version string given")

    def test_default_device(self):
        device = discid.get_default_device()
        self.assertTrue(device is not None, "No default device given")

    def test_features(self):
        features = discid.FEATURES
        self.assertTrue(features is not None, "No feature list given")

    def test_features_implemented(self):
        features = discid.FEATURES_IMPLEMENTED
        self.assertTrue(features, "No feature list implemented")

    def test_invalid_device(self):
        device = "non_existing_device"
        self.assertRaises(discid.DiscError, discid.read, device)

    def test_device_encoding(self):
        device = b"non_existing_device".decode()
        self.assertRaises(discid.DiscError, discid.read, device)
        devicebytes = b"non_existing_device"
        self.assertRaises(discid.DiscError, discid.read, devicebytes)

    def test_put_fail(self):
        # not enough offsets
        self.assertRaises(discid.TOCError, discid.put, 1, 2, 150, [150])
        # too many offsets
        self.assertRaises(discid.TOCError, discid.put, 1, 2, 1000, [150, 500, 750])
        # total sectors / offset mismatch
        self.assertRaises(discid.TOCError, discid.put, 1, 2, 150, [150, 500])
        # too many tracks
        self.assertRaises(discid.TOCError, discid.put, 98, 100, 2000, [150, 500, 750])

    def test_put_success(self):
        for test_disc in test_discs:
            disc = discid.put(
                test_disc.first,
                test_disc.last,
                test_disc.sectors,
                test_disc.offsets,
            )
            self.assertEqual(disc.id, test_disc.id)
            self.assertEqual(disc.freedb_id, test_disc.freedb)
            self.assertEqual(disc.first_track_num, test_disc.first)
            self.assertEqual(disc.last_track_num, test_disc.last)
            self.assertEqual(disc.pregap, test_disc.offsets[0])
            self.assertEqual(disc.sectors, test_disc.sectors)
            track_offsets = [track.offset for track in disc.tracks]
            self.assertEqual(track_offsets, list(test_disc.offsets))
            self.assertEqual(
                disc.sectors, disc.tracks[-1].offset + disc.tracks[-1].sectors
            )
            self.assertEqual(disc.seconds, math.floor((disc.sectors / 75.0) + 0.5))
            self.assertEqual(type(disc.seconds), int)
            for track in disc.tracks:
                self.assertEqual(
                    track.seconds, math.floor((track.sectors / 75.0) + 0.5)
                )
                self.assertEqual(type(track.seconds), int)
            toc = [
                test_disc.first,
                disc.last_track_num,
                disc.sectors,
            ] + track_offsets
            toc_string = " ".join(map(str, toc))
            self.assertEqual(disc.toc_string, toc_string)
            cddb_query = (
                [disc.freedb_id, disc.last_track_num] + track_offsets + [disc.seconds]
            )
            cddb_query_string = " ".join(map(str, cddb_query))
            self.assertEqual(disc.cddb_query_string, cddb_query_string)


@unittest.skipUnless(
    os.environ.get("PYTHON_DISCID_TEST_DEVICE"),
    "tests require physical CD drive with inserted disc",
)
class TestDisc(unittest.TestCase):
    """Test reading the disc currently in the drive"""

    def test_default_device(self):
        # Can't be empty, in contrast to the test in TestModule
        device = discid.get_default_device()
        self.assertTrue(device, "No default device given")

    def test_features(self):
        # Can't be empty, in contrast to the test in TestModule
        features = discid.FEATURES
        self.assertTrue(features, "No feature list given")

    def test_read_simple(self):
        disc = discid.read()  # read from default drive
        self.assertEqual(len(disc.id), 28, "Invalid Disc ID")
        self.assertEqual(len(disc.freedb_id), 8, "Invalid FreeDB Disc ID")
        self.assertTrue(disc.submission_url, "Invalid submission url")
        self.assertTrue(disc.toc_string, "Invalid toc string")
        self.assertEqual(
            disc.last_track_num, len(disc.tracks), "Wrong amount of tracks"
        )
        self.assertEqual(disc.sectors, disc.tracks[-1].offset + disc.tracks[-1].sectors)
        self.assertEqual(disc.pregap, disc.tracks[0].offset)

        for track in disc.tracks:
            self.assertTrue(track.offset <= disc.sectors, "Invalid offset")
            if track.number > 1:
                previous_offset = disc.tracks[track.number - 2].offset
                self.assertTrue(
                    track.offset >= previous_offset, "Invalid offset series"
                )

        # additional features should be unset, not empty
        self.assertTrue(disc.mcn is None)
        for track in disc.tracks:
            self.assertTrue(track.isrc is None)

        # check idempotence (use output again as input to put)
        disc_id = disc.id
        freedb_id = disc.freedb_id
        submission_url = disc.submission_url
        toc_string = disc.toc_string
        first = disc.first_track_num
        last = disc.last_track_num
        sectors = disc.sectors
        track_sectors = [track.sectors for track in disc.tracks]
        track_offsets = [track.offset for track in disc.tracks]
        cddb_query_string = disc.cddb_query_string

        disc = discid.put(first, last, sectors, track_offsets)
        self.assertEqual(disc.id, disc_id, "different id after put")
        self.assertEqual(disc.freedb_id, freedb_id, "different freedb id after put")
        self.assertEqual(
            disc.submission_url, submission_url, "different submission_url after put"
        )
        self.assertEqual(disc.toc_string, toc_string, "different toc_string after put")
        self.assertEqual(disc.first_track_num, first, "different first track after put")
        self.assertEqual(disc.last_track_num, last, "different last track after put")
        self.assertEqual(disc.sectors, sectors, "different sector count after put")
        new_offsets = [track.offset for track in disc.tracks]
        self.assertEqual(new_offsets, track_offsets, "different offsets after put")
        new_sectors = [track.sectors for track in disc.tracks]
        self.assertEqual(new_sectors, track_sectors, "different lengths after put")
        self.assertEqual(
            disc.cddb_query_string,
            cddb_query_string,
            "different cddb_query_string after put",
        )

    def test_read_features(self):
        disc = discid.read(features=["mcn", "isrc"])  # read from default drive
        self.assertEqual(len(disc.id), 28, "Invalid Disc ID")
        self.assertTrue(disc.submission_url, "Invalid submission url")
        self.assertTrue(disc.toc_string, "Invalid toc string")

        if "mcn" in discid.FEATURES:
            self.assertTrue(disc.mcn is not None)
        else:
            self.assertTrue(disc.mcn is None)

        for track in disc.tracks:
            if "isrc" in discid.FEATURES:
                self.assertTrue(track.isrc is not None)
            else:
                self.assertTrue(track.isrc is None)

    def test_read_put(self):
        # a read followed with a put, which should clear the features
        disc = discid.read(features=["mcn", "isrc"])  # read from default drive
        test_disc = test_discs[0]
        disc = discid.put(
            test_disc.first,
            test_disc.last,
            test_disc.sectors,
            test_disc.offsets,
        )
        self.assertTrue(disc.mcn is None)
        for track in disc.tracks:
            self.assertTrue(track.isrc is None)


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


# vim:set shiftwidth=4 smarttab expandtab:
