package com.mzh.library.service;

import com.mzh.library.dao.SystemLogDao;
import com.mzh.library.dao.UserAccountDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.Role;
import com.mzh.library.entity.SystemLog;
import com.mzh.library.entity.SystemLogSearchCriteria;
import com.mzh.library.entity.User;
import com.mzh.library.entity.UserSearchCriteria;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.impl.UserAccountServiceImpl;
import com.mzh.library.util.PasswordHasher;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public final class UserAccountServiceCheck {
    private static final String DENIED_MESSAGE = "您无权管理用户。";
    private static final String UNAVAILABLE_MESSAGE =
        "用户管理服务暂时不可用，请稍后重试。";

    private UserAccountServiceCheck() {
    }

    public static void main(String[] args) {
        Logger.getLogger(UserAccountServiceImpl.class.getName()).setLevel(Level.OFF);

        InMemoryUserAccountDao userDao = new InMemoryUserAccountDao();
        InMemorySystemLogDao logDao = new InMemorySystemLogDao();
        User adminAccount = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, true, "admin-password");
        User staffAccount = user(2L, "staff", "Library Staff", Role.LIBRARIAN, true, "staff-password");
        userDao.put(adminAccount);
        userDao.put(staffAccount);

        UserAccountService service = new UserAccountServiceImpl(userDao, logDao, new PermissionPolicy(),
            new UserAccountServiceImpl.DirectTransactionExecutor());
        AuthenticatedUser admin = actor(1L, Role.ADMINISTRATOR);
        AuthenticatedUser reader = actor(3L, Role.READER);

        requireMessage(service.searchUsers(reader, new UserSearchCriteria()), DENIED_MESSAGE);

        User duplicate = user(0L, " admin ", "Duplicate Admin", Role.ADMINISTRATOR, true, "unused");
        ServiceResult<Long> duplicateResult = service.createUser(admin, duplicate, "new-password", "127.0.0.1");
        require(!duplicateResult.isSuccessful(), "duplicate username should be rejected");
        require(duplicateResult.getErrors().containsKey("username"), "duplicate username should target username");

        User invalid = new User();
        ServiceResult<Long> invalidResult = service.createUser(admin, invalid, "", "127.0.0.1");
        require(!invalidResult.isSuccessful(), "invalid user should be rejected");
        require(invalidResult.getErrors().containsKey("username"), "missing username should be reported");
        require(invalidResult.getErrors().containsKey("displayName"), "missing display name should be reported");
        require(invalidResult.getErrors().containsKey("password"), "missing password should be reported");

        User newReader = new User();
        newReader.setUsername(" new.reader ");
        newReader.setDisplayName("New Reader");
        newReader.setRole(Role.READER);
        newReader.setActive(true);
        ServiceResult<Long> created = service.createUser(admin, newReader, "reader-password", "127.0.0.1");
        require(created.isSuccessful(), "administrator should create user accounts");
        User storedReader = userDao.findById(created.getData()).orElseThrow(AssertionError::new);
        require("new.reader".equals(storedReader.getUsername()), "username should be trimmed like login");
        require(!"reader-password".equals(storedReader.getPasswordHash()), "password should not be stored in plain text");
        require(PasswordHasher.verify("reader-password", storedReader.getPasswordHash()), "stored password should verify");
        require(logDao.logs.size() == 1, "user creation should write one system log");
        require("user.create".equals(logDao.logs.get(0).getOperationType()), "creation log should use user.create");

        User selfRoleChange = user(1L, "admin", "System Administrator", Role.LIBRARIAN, true, "ignored");
        ServiceResult<Void> selfRoleResult = service.updateUser(admin, selfRoleChange, "", "127.0.0.1");
        require(!selfRoleResult.isSuccessful(), "current administrator role change should be blocked");
        require(selfRoleResult.getErrors().containsKey("role"), "self role change should target role");

        User selfDeactivate = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, false, "ignored");
        ServiceResult<Void> selfDeactivateResult = service.updateUser(admin, selfDeactivate, "", "127.0.0.1");
        require(!selfDeactivateResult.isSuccessful(), "current administrator deactivation should be blocked");
        require(selfDeactivateResult.getErrors().containsKey("active"), "self deactivation should target active state");

        String originalStaffHash = staffAccount.getPasswordHash();
        User updatedStaff = user(2L, "staff", "Lead Librarian", Role.LIBRARIAN, true, "ignored");
        ServiceResult<Void> updated = service.updateUser(admin, updatedStaff, "", "127.0.0.1");
        require(updated.isSuccessful(), "administrator should update user accounts");
        require(originalStaffHash.equals(userDao.findById(2L).orElseThrow(AssertionError::new).getPasswordHash()),
            "blank update password should preserve existing hash");

        ServiceResult<Void> reset = service.updateUser(admin, updatedStaff, "replacement-password", "127.0.0.1");
        require(reset.isSuccessful(), "administrator should reset passwords");
        require(PasswordHasher.verify("replacement-password",
            userDao.findById(2L).orElseThrow(AssertionError::new).getPasswordHash()),
            "replacement password should be hashed");

        ServiceResult<Void> deactivated = service.deactivateUser(admin, 2L, "127.0.0.1");
        require(deactivated.isSuccessful(), "administrator should deactivate other accounts");
        require(!userDao.findById(2L).orElseThrow(AssertionError::new).isActive(),
            "deactivate action should mark account inactive");
        require(logDao.logs.stream().anyMatch(log -> "user.deactivate".equals(log.getOperationType())),
            "deactivate should write a system log");

        UserAccountService failingService = new UserAccountServiceImpl(new FailingUserAccountDao(), logDao,
            new PermissionPolicy(), new UserAccountServiceImpl.DirectTransactionExecutor());
        requireMessage(failingService.searchUsers(admin, new UserSearchCriteria()), UNAVAILABLE_MESSAGE);
    }

    private static User user(long id, String username, String displayName, Role role, boolean active, String password) {
        User user = new User();
        user.setId(id);
        user.setUsername(username);
        user.setDisplayName(displayName);
        user.setRole(role);
        user.setActive(active);
        user.setPasswordHash(PasswordHasher.hash(password));
        return user;
    }

    private static AuthenticatedUser actor(long id, Role role) {
        return new AuthenticatedUser(id, role.getCode(), role.getDisplayName(), role,
            role == Role.ADMINISTRATOR
                ? EnumSet.allOf(Permission.class)
                : EnumSet.of(Permission.VIEW_CATALOG, Permission.BORROW_BOOKS));
    }

    private static void requireMessage(ServiceResult<?> result, String message) {
        require(!result.isSuccessful(), "result should be a failure");
        require(message.equals(result.getMessage()), "expected message: " + message);
    }

    private static void require(boolean condition, String message) {
        if (!condition) {
            throw new AssertionError(message);
        }
    }

    private static final class InMemoryUserAccountDao implements UserAccountDao {
        private final Map<Long, User> users = new LinkedHashMap<>();
        private long nextId = 10L;

        private void put(User user) {
            users.put(user.getId(), copy(user));
        }

        @Override
        public List<User> search(UserSearchCriteria criteria) {
            return users.values().stream()
                .filter(user -> criteria.getRoleCode().isEmpty()
                    || user.getRole().getCode().equals(criteria.getRoleCode()))
                .filter(user -> criteria.getActiveValue() == null
                    || user.isActive() == criteria.getActiveValue())
                .map(this::copy)
                .collect(Collectors.toList());
        }

        @Override
        public Optional<User> findById(long id) {
            return Optional.ofNullable(users.get(id)).map(this::copy);
        }

        @Override
        public Optional<User> findByUsername(String username) {
            return users.values().stream()
                .filter(user -> user.getUsername().equals(username))
                .findFirst()
                .map(this::copy);
        }

        @Override
        public long create(Connection connection, User user) {
            long id = nextId++;
            User stored = copy(user);
            stored.setId(id);
            users.put(id, stored);
            return id;
        }

        @Override
        public boolean update(Connection connection, User user, boolean updatePassword) {
            User existing = users.get(user.getId());
            if (existing == null) {
                return false;
            }
            existing.setDisplayName(user.getDisplayName());
            existing.setRole(user.getRole());
            existing.setActive(user.isActive());
            if (updatePassword) {
                existing.setPasswordHash(user.getPasswordHash());
            }
            return true;
        }

        private User copy(User source) {
            User copy = new User();
            copy.setId(source.getId());
            copy.setUsername(source.getUsername());
            copy.setDisplayName(source.getDisplayName());
            copy.setRole(source.getRole());
            copy.setActive(source.isActive());
            copy.setPasswordHash(source.getPasswordHash());
            copy.setCreatedAt(source.getCreatedAt());
            copy.setUpdatedAt(source.getUpdatedAt());
            return copy;
        }
    }

    private static final class InMemorySystemLogDao implements SystemLogDao {
        private final List<SystemLog> logs = new ArrayList<>();

        @Override
        public List<SystemLog> search(SystemLogSearchCriteria criteria) {
            return new ArrayList<>(logs);
        }

        @Override
        public List<String> findOperationTypes() {
            return logs.stream()
                .map(SystemLog::getOperationType)
                .distinct()
                .collect(Collectors.toList());
        }

        @Override
        public long create(Connection connection, SystemLog log) {
            log.setId(logs.size() + 1L);
            logs.add(log);
            return log.getId();
        }
    }

    private static final class FailingUserAccountDao implements UserAccountDao {
        @Override
        public List<User> search(UserSearchCriteria criteria) {
            throw new DaoException("Simulated user search failure", null);
        }

        @Override
        public Optional<User> findById(long id) {
            throw new DaoException("Simulated user lookup failure", null);
        }

        @Override
        public Optional<User> findByUsername(String username) {
            throw new DaoException("Simulated username lookup failure", null);
        }

        @Override
        public long create(Connection connection, User user) {
            throw new DaoException("Simulated user create failure", null);
        }

        @Override
        public boolean update(Connection connection, User user, boolean updatePassword) {
            throw new DaoException("Simulated user update failure", null);
        }
    }
}
