package dev.awesomedev.mirim.service;

import dev.awesomedev.mirim.domain.User;
import dev.awesomedev.mirim.repository.StudentProfileRepository;
import dev.awesomedev.mirim.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
 * 이 파일이 하는 일:
 * AuthService의 핵심 규칙을 검증하는 단위 테스트다. 특히 실제로 났던 버그들의 재발을 막는다.
 *
 * 학습 포인트: 테스트는 "규칙이 있는 곳"에 쓴다.
 * 여기서 검증하는 규칙 — (1) 비밀번호 변경은 정말 저장돼야 한다(오늘 난 버그!),
 * (2) 현재 비밀번호를 틀리면 막는다, (3) 아이디·이메일 중복은 막는다,
 * (4) 멘토 재설정은 학생 계정만 대상.
 *
 * 학습 포인트 ②: PasswordEncoder는 가짜(mock)가 아니라 진짜 BCrypt를 쓴다.
 * "비밀번호가 실제로 맞는지"를 검증하려면 진짜 해시·비교가 필요하기 때문이다.
 * 반대로 DB(리포지토리)는 mock으로 바꿔 DB 없이 빠르게 돈다.
 */
class AuthServiceTest {

    private UserRepository userRepository;
    private StudentProfileRepository studentProfileRepository;
    private PasswordEncoder passwordEncoder;
    private TelegramNotifier telegramNotifier;
    private AuthService authService;

    @BeforeEach
    void setUp() {
        userRepository = mock(UserRepository.class);
        studentProfileRepository = mock(StudentProfileRepository.class);
        passwordEncoder = new BCryptPasswordEncoder(); // 진짜 인코더
        telegramNotifier = mock(TelegramNotifier.class); // 테스트에선 진짜 알림을 보내지 않는다
        authService = new AuthService(userRepository, studentProfileRepository, passwordEncoder, telegramNotifier);
    }

    /** 테스트용 사용자 하나 만들기. User엔 id setter가 없어 리플렉션으로 넣는다. */
    private User userWith(long id, String rawPassword, String role) {
        User user = new User("someone", passwordEncoder.encode(rawPassword), "이름", role, "DEV");
        ReflectionTestUtils.setField(user, "id", id);
        return user;
    }

    // ─────────────────────── 비밀번호 변경 (오늘 난 버그의 회귀 테스트) ───────────────────────

    @Test
    @DisplayName("비밀번호 변경: 현재 비번이 맞으면 새 비번으로 '실제로' 바뀐다")
    void changePassword_actually_updates() {
        // 로그인 사용자(트랜잭션 밖에서 온 detached 객체를 흉내)
        User loginUser = userWith(5L, "oldpass123", "MENTOR");
        // 트랜잭션 안에서 다시 조회될 managed 사용자 — 이 객체가 바뀌어야 진짜 저장된 것
        User managed = userWith(5L, "oldpass123", "MENTOR");
        when(userRepository.findById(5L)).thenReturn(Optional.of(managed));

        authService.changePassword(loginUser, "oldpass123", "newpass456");

        // 핵심 검증 ①: 트랜잭션 안에서 재조회했는가? (detached 객체를 그냥 쓰면 저장이 안 됐던 버그)
        verify(userRepository).findById(5L);
        // 핵심 검증 ②: 재조회한(managed) 사용자의 비밀번호가 실제로 새 값으로 바뀌었는가?
        assertTrue(passwordEncoder.matches("newpass456", managed.getPasswordHash()));
        assertFalse(passwordEncoder.matches("oldpass123", managed.getPasswordHash()));
    }

    @Test
    @DisplayName("비밀번호 변경: 현재 비번이 틀리면 401을 던지고 바꾸지 않는다")
    void changePassword_rejects_wrong_current() {
        User loginUser = userWith(5L, "oldpass123", "MENTOR");
        User managed = userWith(5L, "oldpass123", "MENTOR");
        when(userRepository.findById(5L)).thenReturn(Optional.of(managed));

        ResponseStatusException ex = assertThrows(ResponseStatusException.class,
                () -> authService.changePassword(loginUser, "틀린현재비번", "newpass456"));

        assertEquals(401, ex.getStatusCode().value());
        // 비밀번호는 그대로여야 한다
        assertTrue(passwordEncoder.matches("oldpass123", managed.getPasswordHash()));
    }

    // ─────────────────────── 회원가입 중복 방어 ───────────────────────

    @Test
    @DisplayName("회원가입: 아이디가 이미 있으면 409를 던진다")
    void signup_rejects_duplicate_username() {
        when(userRepository.existsByUsername("honggildong")).thenReturn(true);

        ResponseStatusException ex = assertThrows(ResponseStatusException.class,
                () -> authService.signup(sampleSignup("honggildong", "hong")));

        assertEquals(409, ex.getStatusCode().value());
        verify(userRepository, never()).save(any());
    }

    @Test
    @DisplayName("회원가입: 회사 이메일 아이디가 이미 있으면 409를 던진다")
    void signup_rejects_duplicate_email() {
        when(userRepository.existsByUsername("honggildong")).thenReturn(false);
        when(studentProfileRepository.existsByCompanyEmailLocal("hong")).thenReturn(true);

        ResponseStatusException ex = assertThrows(ResponseStatusException.class,
                () -> authService.signup(sampleSignup("honggildong", "hong")));

        assertEquals(409, ex.getStatusCode().value());
        verify(userRepository, never()).save(any());
    }

    @Test
    @DisplayName("회원가입: 개인정보 동의 없이(false/null) 가입하려 하면 400을 던진다")
    void signup_rejects_without_privacy_consent() {
        // 화면 체크박스를 우회해 API로 직접 요청하는 경우를 흉내 낸다
        ResponseStatusException exFalse = assertThrows(ResponseStatusException.class,
                () -> authService.signup(sampleSignupWithConsent("honggildong", "hong", false)));
        ResponseStatusException exNull = assertThrows(ResponseStatusException.class,
                () -> authService.signup(sampleSignupWithConsent("honggildong", "hong", null)));

        assertEquals(400, exFalse.getStatusCode().value());
        assertEquals(400, exNull.getStatusCode().value());
        verify(userRepository, never()).save(any());
        verify(studentProfileRepository, never()).save(any());
    }

    @Test
    @DisplayName("회원가입: 정상 가입되면 동의 시각이 프로필에 기록된다")
    void signup_records_consent_timestamp() {
        when(userRepository.existsByUsername("honggildong")).thenReturn(false);
        when(studentProfileRepository.existsByCompanyEmailLocal("hong")).thenReturn(false);
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        authService.signup(sampleSignup("honggildong", "hong"));

        // 저장된 프로필을 붙잡아(captor) 동의 시각이 실제로 들어갔는지 확인한다
        var captor = org.mockito.ArgumentCaptor.forClass(dev.awesomedev.mirim.domain.StudentProfile.class);
        verify(studentProfileRepository).save(captor.capture());
        assertNotNull(captor.getValue().getPrivacyConsentAt());
        // 가입 성공 시 멘토에게 텔레그램 알림이 나간다
        verify(telegramNotifier).send(org.mockito.ArgumentMatchers.contains("새 수습생 가입"));
    }

    // ─────────────────────── 멘토 재설정 권한 ───────────────────────

    @Test
    @DisplayName("멘토 재설정: 대상이 멘토(학생 아님)이면 403을 던진다")
    void mentorReset_rejects_non_student() {
        User anotherMentor = userWith(7L, "whatever12", "MENTOR");
        when(userRepository.findById(7L)).thenReturn(Optional.of(anotherMentor));

        ResponseStatusException ex = assertThrows(ResponseStatusException.class,
                () -> authService.resetPasswordByMentor(7L, "newpass456"));

        assertEquals(403, ex.getStatusCode().value());
    }

    @Test
    @DisplayName("멘토 재설정: 대상이 학생이면 새 비번으로 바뀐다")
    void mentorReset_updates_student() {
        User student = userWith(3L, "oldpass123", "STUDENT");
        when(userRepository.findById(3L)).thenReturn(Optional.of(student));

        authService.resetPasswordByMentor(3L, "newpass456");

        assertTrue(passwordEncoder.matches("newpass456", student.getPasswordHash()));
    }

    /** 회원가입 요청 본문 샘플 — 테스트마다 다시 쓰지 않게 한 곳에 모은다. (동의 완료 상태) */
    private dev.awesomedev.mirim.web.dto.SignupRequest sampleSignup(String username, String emailLocal) {
        return sampleSignupWithConsent(username, emailLocal, true);
    }

    private dev.awesomedev.mirim.web.dto.SignupRequest sampleSignupWithConsent(
            String username, String emailLocal, Boolean consent) {
        return new dev.awesomedev.mirim.web.dto.SignupRequest(
                username, "password12", "홍길동", "DEV",
                emailLocal, java.time.LocalDate.of(2007, 3, 15), "010-1234-5678",
                "서울", "미림고", "소프트웨어과",
                "홍판서", "부", "010-9999-8888", consent);
    }
}
