import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from gradebook import (
    DuplicateStudentError,
    Gradebook,
    InvalidScoreError,
    StudentNotFoundError,
)


class TestGradebookNormalPath(unittest.TestCase):
    def test_add_student_returns_student_with_expected_fields(self):
        gradebook = Gradebook()
        student = gradebook.add_student("s1", "Alice")
        self.assertEqual(student.student_id, "s1")
        self.assertEqual(student.name, "Alice")
        self.assertEqual(student.scores, [])

    def test_get_student_returns_added_student(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        student = gradebook.get_student("s1")
        self.assertEqual(student.name, "Alice")

    def test_add_score_records_score(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        gradebook.add_score("s1", 90)
        self.assertEqual(gradebook.get_student("s1").scores, [90])

    def test_all_students_lists_every_registered_student(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        gradebook.add_student("s2", "Bob")
        ids = {s.student_id for s in gradebook.all_students()}
        self.assertEqual(ids, {"s1", "s2"})

    def test_score_boundaries_zero_and_hundred_are_accepted(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        gradebook.add_score("s1", 0)
        gradebook.add_score("s1", 100)
        self.assertEqual(gradebook.get_student("s1").scores, [0, 100])


class TestDuplicateStudentIds(unittest.TestCase):
    def test_adding_duplicate_id_raises(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        with self.assertRaises(DuplicateStudentError):
            gradebook.add_student("s1", "Someone Else")

    def test_duplicate_add_does_not_overwrite_existing_student(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        try:
            gradebook.add_student("s1", "Someone Else")
        except DuplicateStudentError:
            pass
        self.assertEqual(gradebook.get_student("s1").name, "Alice")


class TestInvalidScores(unittest.TestCase):
    def test_negative_score_raises(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        with self.assertRaises(InvalidScoreError):
            gradebook.add_score("s1", -1)

    def test_score_over_100_raises(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        with self.assertRaises(InvalidScoreError):
            gradebook.add_score("s1", 101)

    def test_invalid_score_is_not_recorded(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        with self.assertRaises(InvalidScoreError):
            gradebook.add_score("s1", 150)
        self.assertEqual(gradebook.get_student("s1").scores, [])

    def test_adding_score_for_unknown_student_raises(self):
        gradebook = Gradebook()
        with self.assertRaises(StudentNotFoundError):
            gradebook.add_score("unknown", 50)


class TestAverages(unittest.TestCase):
    def test_average_of_multiple_scores(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        for score in (90, 80, 100):
            gradebook.add_score("s1", score)
        self.assertAlmostEqual(gradebook.average("s1"), 90.0)

    def test_average_with_no_scores_is_zero(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        self.assertEqual(gradebook.average("s1"), 0.0)

    def test_average_with_single_score(self):
        gradebook = Gradebook()
        gradebook.add_student("s1", "Alice")
        gradebook.add_score("s1", 77)
        self.assertAlmostEqual(gradebook.average("s1"), 77.0)

    def test_average_for_unknown_student_raises(self):
        gradebook = Gradebook()
        with self.assertRaises(StudentNotFoundError):
            gradebook.average("unknown")


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