package org.raddatz.familienarchiv.user;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * UserDataInitializer must refuse to seed the admin user with the hardcoded
 * dev defaults when running outside the {@code dev} profile.
 *
 * <p>Why this matters: per DEPLOYMENT.md §3.5 and ADR-011, the admin password
 * is permanently locked on first deploy (UserDataInitializer only seeds when
 * the row is missing). If an operator forgets to set {@code APP_ADMIN_USERNAME}
 * / {@code APP_ADMIN_PASSWORD}, prod silently boots with the well-known dev
 * defaults — a credential-disclosure foot-gun, not a config typo. See #513.
 */
@ExtendWith(MockitoExtension.class)
class AdminSeedFailClosedTest {

    @Mock AppUserRepository userRepository;
    @Mock UserGroupRepository groupRepository;
    @Mock Environment environment;
    @Mock PasswordEncoder passwordEncoder;

    UserDataInitializer initializer;

    @BeforeEach
    void setUp() {
        initializer = new UserDataInitializer(userRepository, groupRepository, environment);
    }

    @Test
    void refuses_to_seed_when_email_is_default_and_profile_is_not_dev() throws Exception {
        when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(environment.matchesProfiles("dev", "test", "e2e")).thenReturn(false);
        ReflectionTestUtils.setField(initializer, "adminEmail", UserDataInitializer.DEFAULT_ADMIN_EMAIL);
        ReflectionTestUtils.setField(initializer, "adminPassword", "operator-set-this-one");

        CommandLineRunner runner = initializer.initAdminUser(passwordEncoder);

        assertThatThrownBy(() -> runner.run())
            .isInstanceOf(IllegalStateException.class)
            .hasMessageContaining("default credentials")
            .hasMessageContaining("permanent");

        verify(userRepository, never()).save(org.mockito.ArgumentMatchers.any());
    }

    @Test
    void refuses_to_seed_when_password_is_default_and_profile_is_not_dev() throws Exception {
        when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(environment.matchesProfiles("dev", "test", "e2e")).thenReturn(false);
        ReflectionTestUtils.setField(initializer, "adminEmail", "admin@archiv.raddatz.cloud");
        ReflectionTestUtils.setField(initializer, "adminPassword", UserDataInitializer.DEFAULT_ADMIN_PASSWORD);

        CommandLineRunner runner = initializer.initAdminUser(passwordEncoder);

        assertThatThrownBy(() -> runner.run())
            .isInstanceOf(IllegalStateException.class)
            .hasMessageContaining("default credentials");
    }

    @Test
    void allows_seed_when_both_values_are_set_and_profile_is_not_dev() throws Exception {
        when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(groupRepository.findByName("Administrators")).thenReturn(Optional.empty());
        when(groupRepository.save(any(UserGroup.class))).thenAnswer(inv -> inv.getArgument(0));
        when(environment.matchesProfiles("dev", "test", "e2e")).thenReturn(false);
        when(passwordEncoder.encode(anyString())).thenReturn("$2a$10$stub");
        ReflectionTestUtils.setField(initializer, "adminEmail", "admin@archiv.raddatz.cloud");
        ReflectionTestUtils.setField(initializer, "adminPassword", "a-real-strong-password");

        CommandLineRunner runner = initializer.initAdminUser(passwordEncoder);
        runner.run();

        verify(userRepository).save(any(AppUser.class));
    }

    @Test
    void allows_seed_with_defaults_when_profile_is_dev() throws Exception {
        when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(groupRepository.findByName("Administrators")).thenReturn(Optional.empty());
        when(groupRepository.save(any(UserGroup.class))).thenAnswer(inv -> inv.getArgument(0));
        when(environment.matchesProfiles("dev", "test", "e2e")).thenReturn(true);
        when(passwordEncoder.encode(anyString())).thenReturn("$2a$10$stub");
        ReflectionTestUtils.setField(initializer, "adminEmail", UserDataInitializer.DEFAULT_ADMIN_EMAIL);
        ReflectionTestUtils.setField(initializer, "adminPassword", UserDataInitializer.DEFAULT_ADMIN_PASSWORD);

        CommandLineRunner runner = initializer.initAdminUser(passwordEncoder);
        runner.run();

        verify(userRepository).save(any(AppUser.class));
    }

    @Test
    void does_not_check_defaults_when_admin_already_exists() throws Exception {
        AppUser existing = AppUser.builder()
            .email("someone@example.com")
            .password("$2a$10$stub")
            .build();
        when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(existing));
        ReflectionTestUtils.setField(initializer, "adminEmail", UserDataInitializer.DEFAULT_ADMIN_EMAIL);
        ReflectionTestUtils.setField(initializer, "adminPassword", UserDataInitializer.DEFAULT_ADMIN_PASSWORD);

        CommandLineRunner runner = initializer.initAdminUser(passwordEncoder);
        runner.run();

        verify(userRepository, never()).save(org.mockito.ArgumentMatchers.any());
        // Importantly, no IllegalStateException — re-deploys must not panic over
        // historical default-seeded data they cannot retroactively fix.
    }

    @Test
    void reuses_existing_Administrators_group_when_seeding_a_new_admin() throws Exception {
        // Setup: admin user does not exist, but the Administrators group does
        // (e.g. previous boot seeded the group then failed; operator deleted
        // the bad user row to retry with a corrected APP_ADMIN_USERNAME). The
        // re-seed must reuse the group, not blind-INSERT a duplicate. See #518.
        UserGroup existingGroup = UserGroup.builder()
            .name("Administrators")
            .build();
        when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(groupRepository.findByName("Administrators")).thenReturn(Optional.of(existingGroup));
        when(environment.matchesProfiles("dev", "test", "e2e")).thenReturn(false);
        when(passwordEncoder.encode(anyString())).thenReturn("$2a$10$stub");
        ReflectionTestUtils.setField(initializer, "adminEmail", "admin@archiv.raddatz.cloud");
        ReflectionTestUtils.setField(initializer, "adminPassword", "a-real-strong-password");

        CommandLineRunner runner = initializer.initAdminUser(passwordEncoder);
        runner.run();

        // Group must not be re-inserted — that would violate user_groups_name_key.
        verify(groupRepository, never()).save(any(UserGroup.class));
        // But the admin user IS created, with the existing group attached.
        org.mockito.ArgumentCaptor<AppUser> captor = org.mockito.ArgumentCaptor.forClass(AppUser.class);
        verify(userRepository).save(captor.capture());
        assertThat(captor.getValue().getGroups()).containsExactly(existingGroup);
    }

    @Test
    void creates_Administrators_group_when_seeding_admin_on_a_fresh_database() throws Exception {
        when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(groupRepository.findByName("Administrators")).thenReturn(Optional.empty());
        when(groupRepository.save(any(UserGroup.class))).thenAnswer(inv -> inv.getArgument(0));
        when(environment.matchesProfiles("dev", "test", "e2e")).thenReturn(false);
        when(passwordEncoder.encode(anyString())).thenReturn("$2a$10$stub");
        ReflectionTestUtils.setField(initializer, "adminEmail", "admin@archiv.raddatz.cloud");
        ReflectionTestUtils.setField(initializer, "adminPassword", "a-real-strong-password");

        CommandLineRunner runner = initializer.initAdminUser(passwordEncoder);
        runner.run();

        // Group should be inserted exactly once.
        verify(groupRepository).save(any(UserGroup.class));
        verify(userRepository).save(any(AppUser.class));
    }
}
