import io
import os
import sys
import unittest
from contextlib import redirect_stdout

# 상위 디렉토리를 Python 경로에 추가
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))


class TestObjectOrientedProgramming(unittest.TestCase):
    """객체 지향 프로그래밍 테스트 클래스"""

    def setUp(self):
        """테스트 전 설정"""

        # Animal 클래스 정의
        class Animal:
            species = "동물"

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

            def speak(self):
                return "동물이 소리를 냅니다."

            def introduce(self):
                return f"저는 {self.name}이고, {self.age}살입니다."

        # Dog 클래스 정의
        class Dog(Animal):
            def speak(self):
                return "멍멍!"

        # Cat 클래스 정의
        class Cat(Animal):
            def speak(self):
                return "야옹~"

        self.Animal = Animal
        self.Dog = Dog
        self.Cat = Cat

    def test_class_attributes(self):
        """클래스 속성 테스트"""
        self.assertEqual(self.Animal.species, "동물")

        # 인스턴스 생성
        animal = self.Animal("테스트", 5)
        self.assertEqual(animal.name, "테스트")
        self.assertEqual(animal.age, 5)

    def test_instance_methods(self):
        """인스턴스 메서드 테스트"""
        animal = self.Animal("테스트동물", 10)

        # introduce 메서드 테스트
        introduction = animal.introduce()
        self.assertEqual(introduction, "저는 테스트동물이고, 10살입니다.")

        # speak 메서드 테스트
        sound = animal.speak()
        self.assertEqual(sound, "동물이 소리를 냅니다.")

    def test_inheritance(self):
        """상속 테스트"""
        # Dog 클래스가 Animal을 상속받는지 확인
        self.assertTrue(issubclass(self.Dog, self.Animal))

        # Cat 클래스가 Animal을 상속받는지 확인
        self.assertTrue(issubclass(self.Cat, self.Animal))

        # Dog 인스턴스 생성 및 테스트
        dog = self.Dog("버디", 5)
        self.assertEqual(dog.name, "버디")
        self.assertEqual(dog.age, 5)
        self.assertEqual(dog.species, "동물")  # 부모 클래스 속성 상속

        # Cat 인스턴스 생성 및 테스트
        cat = self.Cat("루시", 2)
        self.assertEqual(cat.name, "루시")
        self.assertEqual(cat.age, 2)
        self.assertEqual(cat.species, "동물")  # 부모 클래스 속성 상속

    def test_method_overriding(self):
        """메서드 오버라이딩 테스트"""
        animal = self.Animal("일반동물", 1)
        dog = self.Dog("멍멍이", 3)
        cat = self.Cat("고양이", 2)

        # 각각 다른 speak 메서드 결과
        self.assertEqual(animal.speak(), "동물이 소리를 냅니다.")
        self.assertEqual(dog.speak(), "멍멍!")
        self.assertEqual(cat.speak(), "야옹~")

        # 부모 클래스의 메서드는 그대로 상속
        self.assertEqual(dog.introduce(), "저는 멍멍이이고, 3살입니다.")
        self.assertEqual(cat.introduce(), "저는 고양이이고, 2살입니다.")

    def test_polymorphism(self):
        """다형성 테스트"""
        animals = [self.Dog("레오", 4), self.Cat("나비", 1), self.Animal("알수없음", 0)]

        # 각 객체의 speak 메서드가 다르게 동작하는지 확인
        expected_sounds = ["멍멍!", "야옹~", "동물이 소리를 냅니다."]

        for i, animal in enumerate(animals):
            self.assertEqual(animal.speak(), expected_sounds[i])

    def test_object_creation_and_destruction(self):
        """객체 생성 및 소멸 테스트"""
        # 실제 Animal 클래스 import
        import sys
        import os
        sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
        
        try:
            from part_4_object_oriented_programming import Animal
        except ImportError:
            # import 실패 시 테스트 클래스의 Animal 사용
            Animal = self.Animal
        
        # 객체 생성 시 출력 확인
        f = io.StringIO()
        with redirect_stdout(f):
            animal = Animal("생성테스트", 5)
    
        output = f.getvalue()
        self.assertIn("생성테스트", output)

    def test_multiple_instances(self):
        """여러 인스턴스 생성 테스트"""
        # 여러 동물 인스턴스 생성
        animals = [
            self.Animal("동물1", 1),
            self.Dog("강아지1", 2),
            self.Cat("고양이1", 3),
            self.Dog("강아지2", 4),
            self.Cat("고양이2", 5),
        ]

        # 각 인스턴스가 독립적인지 확인
        self.assertEqual(animals[0].name, "동물1")
        self.assertEqual(animals[1].name, "강아지1")
        self.assertEqual(animals[2].name, "고양이1")
        self.assertEqual(animals[3].name, "강아지2")
        self.assertEqual(animals[4].name, "고양이2")

        # 각 인스턴스의 나이가 올바른지 확인
        self.assertEqual(animals[0].age, 1)
        self.assertEqual(animals[1].age, 2)
        self.assertEqual(animals[2].age, 3)
        self.assertEqual(animals[3].age, 4)
        self.assertEqual(animals[4].age, 5)

    def test_class_hierarchy(self):
        """클래스 계층 구조 테스트"""
        # isinstance 테스트
        dog = self.Dog("테스트", 1)
        cat = self.Cat("테스트", 1)
        animal = self.Animal("테스트", 1)

        # Dog는 Animal의 인스턴스
        self.assertTrue(isinstance(dog, self.Animal))
        self.assertTrue(isinstance(dog, self.Dog))

        # Cat은 Animal의 인스턴스
        self.assertTrue(isinstance(cat, self.Animal))
        self.assertTrue(isinstance(cat, self.Cat))

        # Animal은 Animal의 인스턴스
        self.assertTrue(isinstance(animal, self.Animal))
        self.assertFalse(isinstance(animal, self.Dog))
        self.assertFalse(isinstance(animal, self.Cat))

    def test_attribute_access(self):
        """속성 접근 테스트"""
        animal = self.Animal("테스트", 10)

        # 인스턴스 속성 접근
        self.assertEqual(animal.name, "테스트")
        self.assertEqual(animal.age, 10)

        # 클래스 속성 접근
        self.assertEqual(animal.species, "동물")
        self.assertEqual(self.Animal.species, "동물")

        # 속성 변경 테스트
        animal.name = "새이름"
        animal.age = 20
        self.assertEqual(animal.name, "새이름")
        self.assertEqual(animal.age, 20)


class TestAdvancedOOP(unittest.TestCase):
    """고급 객체 지향 프로그래밍 테스트 클래스"""

    def test_encapsulation(self):
        """캡슐화 테스트"""

        class BankAccount:
            def __init__(self, balance):
                self._balance = balance  # protected attribute

            def get_balance(self):
                return self._balance

            def deposit(self, amount):
                if amount > 0:
                    self._balance += amount
                    return True
                return False

            def withdraw(self, amount):
                if 0 < amount <= self._balance:
                    self._balance -= amount
                    return True
                return False

        # 계좌 생성
        account = BankAccount(1000)

        # 초기 잔액 확인
        self.assertEqual(account.get_balance(), 1000)

        # 입금 테스트
        self.assertTrue(account.deposit(500))
        self.assertEqual(account.get_balance(), 1500)

        # 출금 테스트
        self.assertTrue(account.withdraw(300))
        self.assertEqual(account.get_balance(), 1200)

        # 잘못된 출금 테스트
        self.assertFalse(account.withdraw(2000))
        self.assertEqual(account.get_balance(), 1200)  # 잔액 변경 없음

    def test_static_methods(self):
        """정적 메서드 테스트"""

        class MathUtils:
            @staticmethod
            def add(a, b):
                return a + b

            @staticmethod
            def multiply(a, b):
                return a * b

            @staticmethod
            def is_even(num):
                return num % 2 == 0

        # 정적 메서드 호출
        self.assertEqual(MathUtils.add(5, 3), 8)
        self.assertEqual(MathUtils.multiply(4, 6), 24)
        self.assertTrue(MathUtils.is_even(10))
        self.assertFalse(MathUtils.is_even(7))

    def test_property_decorator(self):
        """프로퍼티 데코레이터 테스트"""

        class Circle:
            def __init__(self, radius):
                self._radius = radius

            @property
            def radius(self):
                return self._radius

            @radius.setter
            def radius(self, value):
                if value > 0:
                    self._radius = value
                else:
                    raise ValueError("반지름은 양수여야 합니다.")

            @property
            def area(self):
                import math

                return math.pi * self._radius**2

        # 원 생성
        circle = Circle(5)

        # 반지름 접근
        self.assertEqual(circle.radius, 5)

        # 면적 계산
        import math

        expected_area = math.pi * 5**2
        self.assertAlmostEqual(circle.area, expected_area)

        # 반지름 변경
        circle.radius = 10
        self.assertEqual(circle.radius, 10)

        # 잘못된 반지름 설정
        with self.assertRaises(ValueError):
            circle.radius = -5


if __name__ == "__main__":
    # 테스트 스위트 생성
    test_suite = unittest.TestSuite()

    # 테스트 클래스 추가
    test_suite.addTest(unittest.makeSuite(TestObjectOrientedProgramming))
    test_suite.addTest(unittest.makeSuite(TestAdvancedOOP))

    # 테스트 실행
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(test_suite)

    # 테스트 결과 출력
    print(f"\n테스트 결과: {len(result.failures)} 실패, {len(result.errors)} 오류")
    if result.failures:
        print("\n실패한 테스트:")
        for test, traceback in result.failures:
            print(f"- {test}: {traceback}")
    if result.errors:
        print("\n오류가 발생한 테스트:")
        for test, traceback in result.errors:
            print(f"- {test}: {traceback}")
