package com.ttstd.signaling;

import com.ttstd.signaling.model.AuthPrincipal;
import com.ttstd.signaling.model.DeviceType;
import com.ttstd.signaling.model.PrincipalType;
import com.ttstd.signaling.security.AuthException;
import com.ttstd.signaling.security.JwtService;
import com.ttstd.signaling.security.SecurityProperties;
import com.ttstd.signaling.security.TotpService;
import com.ttstd.signaling.service.AccountService;
import com.ttstd.signaling.service.AuditService;
import com.ttstd.signaling.service.DeviceIdentityService;
import com.ttstd.signaling.service.TokenPair;
import com.ttstd.signaling.store.InMemoryDeviceStore;
import com.ttstd.signaling.store.InMemoryNonceStore;
import com.ttstd.signaling.store.InMemorySessionStore;
import com.ttstd.signaling.store.InMemoryUserStore;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.HexFormat;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * 账号机制与握手鉴权核心逻辑测试。
 */
class AuthFlowTest {

    private static final String PROVISION_SECRET = "test-provision-secret-value-0123456789";

    private SecurityProperties properties;
    private AccountService accountService;
    private DeviceIdentityService deviceIdentityService;

    @BeforeEach
    void setUp() {
        properties = new SecurityProperties();
        properties.getJwt().setSecret("test-jwt-secret-key-must-be-at-least-32-bytes-long");
        properties.getDevice().setProvisionSecret(PROVISION_SECRET);
        properties.getAccount().setBootstrapPassword(null);

        JwtService jwtService = new JwtService(properties);
        TotpService totpService = new TotpService();
        AuditService auditService = new AuditService();
        accountService = new AccountService(
                new InMemoryUserStore(), new InMemorySessionStore(), properties,
                jwtService, totpService, auditService);
        deviceIdentityService = new DeviceIdentityService(
                new InMemoryDeviceStore(), new InMemoryNonceStore(), properties, jwtService);
    }

    // ==================== 账号 ====================

    @Test
    void loginReturnsUsableAccessToken() {
        accountService.register("alice", "Xk9#mP2$vLq7");
        TokenPair pair = accountService.login("alice", "Xk9#mP2$vLq7", "127.0.0.1", "junit");

        assertNotNull(pair.accessToken());
        AuthPrincipal principal = accountService.authenticate(pair.accessToken());
        assertEquals(PrincipalType.USER, principal.principalType());
        assertEquals(DeviceType.CONTROLLER, principal.deviceType());
        assertEquals("alice", principal.displayName());
        // 主控端信令 ID 由服务端派生，不可由客户端指定
        assertTrue(principal.deviceId().startsWith("ctl_"));
    }

    @Test
    void wrongPasswordIsRejected() {
        accountService.register("bob", "Xk9#mP2$vLq7");
        assertThrows(AuthException.class,
                () -> accountService.login("bob", "WrongPass1!", "127.0.0.1", "junit"));
    }

    @Test
    void weakPasswordIsRejected() {
        assertThrows(AuthException.class, () -> accountService.register("carol", "short"));
    }

    @Test
    void duplicateUsernameIsRejected() {
        accountService.register("dave", "Xk9#mP2$vLq7");
        assertThrows(AuthException.class, () -> accountService.register("dave", "Xk9#mP2$vLq7"));
    }

    /** 封禁后：已签发的 access token 必须立即失效。 */
    @Test
    void banInvalidatesExistingAccessToken() {
        accountService.register("eve", "Xk9#mP2$vLq7");
        TokenPair pair = accountService.login("eve", "Xk9#mP2$vLq7", "127.0.0.1", "junit");
        String userId = pair.principalId();

        assertNotNull(accountService.authenticate(pair.accessToken()));

        accountService.ban(userId, null, "违规操作");

        assertThrows(AuthException.class, () -> accountService.authenticate(pair.accessToken()));
        assertThrows(AuthException.class,
                () -> accountService.login("eve", "Xk9#mP2$vLq7", "127.0.0.1", "junit"));
    }

    /** 临时封禁到期后应自动恢复。 */
    @Test
    void expiredSuspensionAutoRecovers() {
        accountService.register("frank", "Xk9#mP2$vLq7");
        TokenPair pair = accountService.login("frank", "Xk9#mP2$vLq7", "127.0.0.1", "junit");
        accountService.ban(pair.principalId(), Instant.now().minusSeconds(1), "临时");

        // 封禁已过期，应能重新登录
        assertNotNull(accountService.login("frank", "Xk9#mP2$vLq7", "127.0.0.1", "junit"));
    }

    /** 踢出会话后，该会话的 access token 立即不可用。 */
    @Test
    void revokeSessionInvalidatesToken() {
        accountService.register("grace", "Xk9#mP2$vLq7");
        TokenPair pair = accountService.login("grace", "Xk9#mP2$vLq7", "127.0.0.1", "junit");

        accountService.revokeSession(pair.sessionId(), "管理员踢出");

        assertThrows(AuthException.class, () -> accountService.authenticate(pair.accessToken()));
    }

    /** 刷新令牌应轮转，且旧令牌复用会导致整个会话被吊销。 */
    @Test
    void refreshTokenRotatesAndDetectsReuse() {
        accountService.register("heidi", "Xk9#mP2$vLq7");
        TokenPair first = accountService.login("heidi", "Xk9#mP2$vLq7", "127.0.0.1", "junit");

        TokenPair second = accountService.refresh(first.refreshToken(), "127.0.0.1", "junit");
        assertNotEquals(first.refreshToken(), second.refreshToken());

        // 复用旧刷新令牌 -> 判定泄露，吊销会话
        assertThrows(AuthException.class,
                () -> accountService.refresh(first.refreshToken(), "127.0.0.1", "junit"));
        // 会话已被吊销，新刷新令牌同样失效
        assertThrows(AuthException.class,
                () -> accountService.refresh(second.refreshToken(), "127.0.0.1", "junit"));
    }

    /** 连续登录失败应触发锁定。 */
    @Test
    void repeatedFailuresLockAccount() {
        accountService.register("ivan", "Xk9#mP2$vLq7");
        for (int i = 0; i < properties.getAccount().getMaxFailedAttempts(); i++) {
            assertThrows(AuthException.class,
                    () -> accountService.login("ivan", "Bad1Pass!", "127.0.0.1", "junit"));
        }
        // 即便密码正确，锁定期内也应拒绝
        AuthException ex = assertThrows(AuthException.class,
                () -> accountService.login("ivan", "Xk9#mP2$vLq7", "127.0.0.1", "junit"));
        assertEquals(429, ex.getStatus());
    }

    /** 令牌用途隔离：refresh 令牌不可当作 access 令牌使用。 */
    @Test
    void deviceTokenCannotAuthenticateAsUser() {
        DeviceIdentityService.ProvisionResult provisioned = provisionDevice("SN-TEST-0001");
        TokenPair deviceToken = deviceIdentityService.issueDeviceToken(
                provisioned.deviceUid(), provisioned.deviceSecret());

        assertThrows(AuthException.class,
                () -> accountService.authenticate(deviceToken.accessToken()));
    }

    // ==================== 设备 ====================

    @Test
    void provisionAndAuthenticateDevice() {
        DeviceIdentityService.ProvisionResult result = provisionDevice("SN-TEST-1234");
        assertTrue(result.deviceUid().startsWith("dev_"));

        TokenPair pair = deviceIdentityService.issueDeviceToken(
                result.deviceUid(), result.deviceSecret());
        AuthPrincipal principal = deviceIdentityService.authenticate(pair.accessToken());

        assertEquals(PrincipalType.DEVICE, principal.principalType());
        assertEquals(DeviceType.CONTROLLED, principal.deviceType());
        // 对外暴露的是 deviceUid，而非 SN
        assertEquals(result.deviceUid(), principal.deviceId());
    }

    @Test
    void provisionWithBadHmacIsRejected() {
        long ts = Instant.now().getEpochSecond();
        assertThrows(AuthException.class, () -> deviceIdentityService.provision(
                "SN-BAD", "model", UUID.randomUUID().toString(), ts, "deadbeef"));
    }

    /** nonce 重放必须被拒绝。 */
    @Test
    void provisionReplayIsRejected() {
        String sn = "SN-REPLAY";
        String nonce = UUID.randomUUID().toString();
        long ts = Instant.now().getEpochSecond();
        String hmac = hmacHex(PROVISION_SECRET, sn + "|" + nonce + "|" + ts);

        deviceIdentityService.provision(sn, "model", nonce, ts, hmac);
        assertThrows(AuthException.class,
                () -> deviceIdentityService.provision(sn, "model", nonce, ts, hmac));
    }

    /** 过期时间戳必须被拒绝。 */
    @Test
    void provisionWithStaleTimestampIsRejected() {
        String sn = "SN-STALE";
        String nonce = UUID.randomUUID().toString();
        long ts = Instant.now().getEpochSecond() - 99999;
        String hmac = hmacHex(PROVISION_SECRET, sn + "|" + nonce + "|" + ts);

        assertThrows(AuthException.class,
                () -> deviceIdentityService.provision(sn, "model", nonce, ts, hmac));
    }

    /** SN 白名单启用后，未授权 SN 不可激活。 */
    @Test
    void allowlistBlocksUnknownSn() {
        properties.getDevice().setSnAllowlistEnabled(true);
        assertThrows(AuthException.class, () -> provisionDevice("SN-NOT-ALLOWED"));

        deviceIdentityService.importAllowlist(java.util.List.of("SN-ALLOWED"));
        assertNotNull(provisionDevice("SN-ALLOWED"));
    }

    /** 禁用设备后其令牌立即失效。 */
    @Test
    void disablingDeviceInvalidatesToken() {
        DeviceIdentityService.ProvisionResult result = provisionDevice("SN-DISABLE");
        TokenPair pair = deviceIdentityService.issueDeviceToken(
                result.deviceUid(), result.deviceSecret());
        assertNotNull(deviceIdentityService.authenticate(pair.accessToken()));

        deviceIdentityService.disable(result.deviceUid(), null, "设备丢失");

        assertThrows(AuthException.class,
                () -> deviceIdentityService.authenticate(pair.accessToken()));
    }

    /** 重新激活会轮换密钥，旧 deviceSecret 失效。 */
    @Test
    void reProvisionRotatesSecret() {
        DeviceIdentityService.ProvisionResult first = provisionDevice("SN-ROTATE");
        DeviceIdentityService.ProvisionResult second = provisionDevice("SN-ROTATE");

        // 同一 SN 复用同一 deviceUid，但密钥已轮换
        assertEquals(first.deviceUid(), second.deviceUid());
        assertNotEquals(first.deviceSecret(), second.deviceSecret());

        assertThrows(AuthException.class,
                () -> deviceIdentityService.issueDeviceToken(first.deviceUid(), first.deviceSecret()));
        assertNotNull(deviceIdentityService.issueDeviceToken(
                second.deviceUid(), second.deviceSecret()));
    }

    @Test
    void tamperedTokenIsRejected() {
        accountService.register("judy", "Xk9#mP2$vLq7");
        TokenPair pair = accountService.login("judy", "Xk9#mP2$vLq7", "127.0.0.1", "junit");

        String tampered = pair.accessToken().substring(0, pair.accessToken().length() - 3) + "aaa";
        assertThrows(AuthException.class, () -> accountService.authenticate(tampered));
    }

    // ==================== helpers ====================

    private DeviceIdentityService.ProvisionResult provisionDevice(String sn) {
        String nonce = UUID.randomUUID().toString();
        long ts = Instant.now().getEpochSecond();
        String hmac = hmacHex(PROVISION_SECRET, sn + "|" + nonce + "|" + ts);
        return deviceIdentityService.provision(sn, "test-model", nonce, ts, hmac);
    }

    private static String hmacHex(String key, String data) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            return HexFormat.of().formatHex(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }
}
