import type { Pool as MariaPool } from 'mariadb';
import type { Pool as PgPool, PoolClient, QueryResult } from 'pg';
import { describe, expect, it, vi } from 'vitest';

import { upsertRows } from '../src/db.js';
import { mapMember, MEMBER_PRESERVED_COLUMNS, migrateGateway, preflightMemberConflicts } from '../src/gateway.js';

const memberRow = (overrides: Record<string, unknown> = {}) => ({
    NO: 7,
    GRADE: 1,
    acl: '{}',
    penalty: '{}',
    oauth_info: '{}',
    oauth_type: 'KAKAO',
    oauth_id: null,
    PW: 'a'.repeat(128),
    salt: 'member-salt',
    ID: 'LegacyUser',
    NAME: '레거시유저',
    EMAIL: 'USER@EXAMPLE.TEST',
    PICTURE: 'default.jpg',
    IMGSVR: 0,
    third_use: 0,
    token_valid_until: null,
    delete_after: null,
    REG_DATE: '2020-01-01 00:00:00',
    REG_NUM: 0,
    BLOCK_NUM: 0,
    BLOCK_DATE: null,
    ...overrides,
});

describe('legacy gateway member migration', () => {
    it('marks imported SHA-512 credentials for reset without trusting a missing Kakao ID', () => {
        const mapped = mapMember(memberRow(), new Date('2026-08-17T00:00:00.000Z'), null);

        expect(mapped).toMatchObject({
            login_id: 'legacyuser',
            email: 'user@example.test',
            password_reset_required: true,
            oauth_type: 'KAKAO',
            oauth_id: null,
            kakao_verified_at: null,
        });
    });

    it('preserves target-owned credentials and OAuth state on a repeated member upsert', async () => {
        const query = vi.fn(async (_sql: string, _values?: unknown[]) => ({ rows: [], rowCount: 0 }));
        const client = { query } as unknown as PoolClient;

        await upsertRows(
            client,
            'app_user',
            [
                {
                    id: 'legacy-id',
                    login_id: 'legacy-user',
                    password_hash: 'legacy-hash',
                    oauth_info: '{}',
                    legacy_data: '{}',
                },
            ],
            ['id'],
            { preserveOnConflict: ['password_hash', 'oauth_info'] }
        );

        const sql = String(query.mock.calls[0]?.[0]);
        expect(sql).toContain('"login_id" = EXCLUDED."login_id"');
        expect(sql).toContain('"legacy_data" = EXCLUDED."legacy_data"');
        expect(sql).not.toContain('"password_hash" = EXCLUDED."password_hash"');
        expect(sql).not.toContain('"oauth_info" = EXCLUDED."oauth_info"');
    });

    it('preserves renamed login and display identities along with every live credential field', () => {
        expect(MEMBER_PRESERVED_COLUMNS).toEqual(
            expect.arrayContaining([
                'login_id',
                'display_name',
                'password_hash',
                'password_salt',
                'password_reset_required',
                'roles',
                'sanctions',
                'oauth_id',
                'email',
                'updated_at',
                'last_login_at',
                'created_at',
            ])
        );
        expect(MEMBER_PRESERVED_COLUMNS).not.toContain('legacy_data');
    });

    it('rejects a source member when another target account already owns its identity', async () => {
        const query = vi.fn(async (..._args: unknown[]) => ({
            rows: [
                {
                    id: 'another-target-id',
                    login_id: 'legacyuser',
                    display_name: '다른사용자',
                    email: 'other@example.test',
                },
            ],
            rowCount: 1,
        }));
        const client = { query } as unknown as PoolClient;
        const mapped = mapMember(memberRow({ oauth_id: 'stable-kakao-id' }), new Date('2026-08-17T00:00:00Z'), null);

        await expect(preflightMemberConflicts(client, [mapped])).rejects.toThrow(
            'Target account identity collision in legacy member batch'
        );
        expect(String(query.mock.calls[0]?.[0])).toContain('FROM "app_user"');
    });

    it('reads only new immutable member logs during an incremental dry-run', async () => {
        const fingerprint = 'a'.repeat(64);
        const seen = new Set<string>();
        const source = {
            query: vi.fn(async (sql: string, values: readonly unknown[] = []) => {
                if (sql.includes('MAX(`id`)') && sql.includes('member_log')) return [{ max_id: 2 }];
                if (sql.includes('MAX(`date`)')) return [];
                if (sql.includes('FROM `member`')) return [];
                if (sql.includes('FROM `member_log`')) {
                    if (seen.has('member_log')) return [];
                    seen.add('member_log');
                    return Number(values[0]) < 2
                        ? [
                              {
                                  id: 2,
                                  member_no: 7,
                                  date: new Date('2026-08-18T00:00:00.000Z'),
                                  action_type: 'login',
                                  action: null,
                              },
                          ]
                        : [];
                }
                return [];
            }),
        } as unknown as MariaPool;
        const query = vi.fn(async (sql: string) => {
            if (sql.includes('FROM "legacy_import_checkpoint"')) {
                return {
                    rows: [{ source_fingerprint: fingerprint, last_legacy_id: '1' }],
                    rowCount: 1,
                } as QueryResult;
            }
            return { rows: [], rowCount: 0 } as unknown as QueryResult;
        });
        const client = { query, release: vi.fn() } as unknown as PoolClient;
        const target = { connect: vi.fn(async () => client) } as unknown as PgPool;

        const summary = await migrateGateway(source, target, false, new Date('2026-08-18T00:00:00.000Z'), {
            mode: 'incremental',
            source: { key: 'fixture:gateway', fingerprint },
        });

        expect(summary.counts.member_log).toBe(1);
        expect(summary.progress?.member_log).toMatchObject({
            strategy: 'append',
            startAfterId: '1',
            endAtId: '2',
            processed: 1,
        });
        expect(query.mock.calls.some(([sql]) => String(sql).includes('INSERT INTO "legacy_member_log"'))).toBe(false);
    });
});
