const { describe, test, expect, beforeEach, afterEach } = require('bun:test')
const crypto = require('crypto')
const path = require('path')
const fs = require('fs')
const Database = require('better-sqlite3')

const testDbPath = path.join(__dirname, '.test-sessions.db')

let testDb

function createTestSchema(db) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS sessions (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      token TEXT NOT NULL UNIQUE,
      refresh_token_hash TEXT,
      device_fingerprint TEXT,
      ip_address TEXT,
      user_agent TEXT,
      expires_at INTEGER NOT NULL,
      created_at INTEGER NOT NULL,
      last_accessed_at INTEGER NOT NULL
    )
  `)
  
  db.exec(`
    CREATE TABLE IF NOT EXISTS refresh_tokens (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      session_id TEXT NOT NULL,
      token_hash TEXT NOT NULL UNIQUE,
      device_fingerprint TEXT,
      ip_address TEXT,
      user_agent TEXT,
      used INTEGER DEFAULT 0,
      revoked INTEGER DEFAULT 0,
      expires_at INTEGER NOT NULL,
      created_at INTEGER NOT NULL,
      used_at INTEGER
    )
  `)
  
  db.exec(`
    CREATE TABLE IF NOT EXISTS token_blacklist (
      id TEXT PRIMARY KEY,
      token_jti TEXT NOT NULL UNIQUE,
      user_id TEXT,
      expires_at INTEGER NOT NULL,
      created_at INTEGER NOT NULL,
      reason TEXT
    )
  `)
}

describe('Session Repository', () => {
  beforeEach(() => {
    if (fs.existsSync(testDbPath)) {
      fs.unlinkSync(testDbPath)
    }
    
    testDb = new Database(testDbPath)
    createTestSchema(testDb)
  })

  afterEach(() => {
    if (testDb) {
      testDb.close()
    }
    if (fs.existsSync(testDbPath)) {
      fs.unlinkSync(testDbPath)
    }
  })

  describe('Session CRUD Operations', () => {
    test('creates a session', () => {
      const now = Date.now()
      const sessionId = crypto.randomUUID()
      const token = crypto.randomBytes(32).toString('hex')
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run(sessionId, 'user-1', token, now + 3600000, now, now)
      
      const session = testDb.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId)
      
      expect(session).toBeDefined()
      expect(session.user_id).toBe('user-1')
      expect(session.token).toBe(token)
    })

    test('creates session with all metadata', () => {
      const now = Date.now()
      const sessionId = crypto.randomUUID()
      const token = crypto.randomBytes(32).toString('hex')
      const refreshTokenHash = crypto.randomBytes(64).toString('hex')
      const fingerprint = crypto.randomBytes(16).toString('hex')
      
      testDb.prepare(`
        INSERT INTO sessions (
          id, user_id, token, refresh_token_hash, device_fingerprint,
          ip_address, user_agent, expires_at, created_at, last_accessed_at
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        sessionId, 'user-2', token, refreshTokenHash, fingerprint,
        '192.168.1.1', 'Mozilla/5.0', now + 3600000, now, now
      )
      
      const session = testDb.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId)
      
      expect(session.device_fingerprint).toBe(fingerprint)
      expect(session.ip_address).toBe('192.168.1.1')
      expect(session.user_agent).toBe('Mozilla/5.0')
    })

    test('gets session by ID', () => {
      const now = Date.now()
      const sessionId = crypto.randomUUID()
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run(sessionId, 'user-3', 'token-3', now + 3600000, now, now)
      
      const session = testDb.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId)
      
      expect(session).toBeDefined()
      expect(session.id).toBe(sessionId)
    })

    test('gets session by token', () => {
      const now = Date.now()
      const token = crypto.randomBytes(32).toString('hex')
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-4', 'user-4', token, now + 3600000, now, now)
      
      const session = testDb.prepare('SELECT * FROM sessions WHERE token = ?').get(token)
      
      expect(session).toBeDefined()
      expect(session.id).toBe('session-4')
    })

    test('gets all sessions for user', () => {
      const now = Date.now()
      
      for (let i = 0; i < 3; i++) {
        testDb.prepare(`
          INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
          VALUES (?, ?, ?, ?, ?, ?)
        `).run(`session-${i}`, 'user-5', `token-5-${i}`, now + 3600000, now, now)
      }
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-other', 'user-other', 'token-other', now + 3600000, now, now)
      
      const sessions = testDb.prepare(`
        SELECT * FROM sessions WHERE user_id = ? AND expires_at > ? ORDER BY last_accessed_at DESC
      `).all('user-5', now)
      
      expect(sessions.length).toBe(3)
    })

    test('excludes expired sessions', () => {
      const now = Date.now()
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-active', 'user-6', 'token-active', now + 3600000, now, now)
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-expired', 'user-6', 'token-expired', now - 1000, now, now)
      
      const sessions = testDb.prepare(`
        SELECT * FROM sessions WHERE user_id = ? AND expires_at > ?
      `).all('user-6', now)
      
      expect(sessions.length).toBe(1)
      expect(sessions[0].id).toBe('session-active')
    })

    test('updates session last accessed time', () => {
      const now = Date.now()
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-update', 'user-7', 'token-7', now + 3600000, now, now - 1000)
      
      const newTime = Date.now()
      testDb.prepare('UPDATE sessions SET last_accessed_at = ? WHERE id = ?')
        .run(newTime, 'session-update')
      
      const session = testDb.prepare('SELECT * FROM sessions WHERE id = ?').get('session-update')
      
      expect(session.last_accessed_at).toBe(newTime)
    })

    test('deletes session (logout)', () => {
      const now = Date.now()
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-delete', 'user-8', 'token-8', now + 3600000, now, now)
      
      const result = testDb.prepare('DELETE FROM sessions WHERE id = ?').run('session-delete')
      
      expect(result.changes).toBe(1)
      
      const session = testDb.prepare('SELECT * FROM sessions WHERE id = ?').get('session-delete')
      expect(session).toBeUndefined()
    })

    test('deletes all sessions for user (logout all)', () => {
      const now = Date.now()
      
      for (let i = 0; i < 5; i++) {
        testDb.prepare(`
          INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
          VALUES (?, ?, ?, ?, ?, ?)
        `).run(`session-del-${i}`, 'user-9', `token-9-${i}`, now + 3600000, now, now)
      }
      
      const result = testDb.prepare('DELETE FROM sessions WHERE user_id = ?').run('user-9')
      
      expect(result.changes).toBe(5)
    })

    test('cleans up expired sessions', () => {
      const now = Date.now()
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-cleanup-1', 'user-10', 'token-10-1', now - 1000, now, now)
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-cleanup-2', 'user-10', 'token-10-2', now + 3600000, now, now)
      
      const result = testDb.prepare('DELETE FROM sessions WHERE expires_at <= ?').run(now)
      
      expect(result.changes).toBe(1)
    })
  })

  describe('Refresh Token Operations', () => {
    test('creates refresh token', () => {
      const now = Date.now()
      const tokenId = crypto.randomUUID()
      const tokenHash = crypto.randomBytes(64).toString('hex')
      
      testDb.prepare(`
        INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run(tokenId, 'user-rt-1', 'session-rt-1', tokenHash, now + 604800000, now)
      
      const token = testDb.prepare('SELECT * FROM refresh_tokens WHERE id = ?').get(tokenId)
      
      expect(token).toBeDefined()
      expect(token.user_id).toBe('user-rt-1')
      expect(token.used).toBe(0)
      expect(token.revoked).toBe(0)
    })

    test('gets refresh token by hash', () => {
      const now = Date.now()
      const tokenHash = crypto.randomBytes(64).toString('hex')
      
      testDb.prepare(`
        INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('rt-2', 'user-rt-2', 'session-rt-2', tokenHash, now + 604800000, now)
      
      const token = testDb.prepare(`
        SELECT * FROM refresh_tokens 
        WHERE token_hash = ? AND used = 0 AND revoked = 0 AND expires_at > ?
      `).get(tokenHash, now)
      
      expect(token).toBeDefined()
      expect(token.id).toBe('rt-2')
    })

    test('excludes used refresh tokens', () => {
      const now = Date.now()
      const tokenHash = crypto.randomBytes(64).toString('hex')
      
      testDb.prepare(`
        INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at, used)
        VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run('rt-used', 'user-rt-3', 'session-rt-3', tokenHash, now + 604800000, now, 1)
      
      const token = testDb.prepare(`
        SELECT * FROM refresh_tokens 
        WHERE token_hash = ? AND used = 0 AND revoked = 0 AND expires_at > ?
      `).get(tokenHash, now)
      
      expect(token).toBeUndefined()
    })

    test('excludes revoked refresh tokens', () => {
      const now = Date.now()
      const tokenHash = crypto.randomBytes(64).toString('hex')
      
      testDb.prepare(`
        INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at, revoked)
        VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run('rt-revoked', 'user-rt-4', 'session-rt-4', tokenHash, now + 604800000, now, 1)
      
      const token = testDb.prepare(`
        SELECT * FROM refresh_tokens 
        WHERE token_hash = ? AND used = 0 AND revoked = 0 AND expires_at > ?
      `).get(tokenHash, now)
      
      expect(token).toBeUndefined()
    })

    test('excludes expired refresh tokens', () => {
      const now = Date.now()
      const tokenHash = crypto.randomBytes(64).toString('hex')
      
      testDb.prepare(`
        INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('rt-expired', 'user-rt-5', 'session-rt-5', tokenHash, now - 1000, now)
      
      const token = testDb.prepare(`
        SELECT * FROM refresh_tokens 
        WHERE token_hash = ? AND used = 0 AND revoked = 0 AND expires_at > ?
      `).get(tokenHash, now)
      
      expect(token).toBeUndefined()
    })

    test('marks refresh token as used', () => {
      const now = Date.now()
      const tokenHash = crypto.randomBytes(64).toString('hex')
      
      testDb.prepare(`
        INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('rt-mark-used', 'user-rt-6', 'session-rt-6', tokenHash, now + 604800000, now)
      
      const usedAt = Date.now()
      testDb.prepare('UPDATE refresh_tokens SET used = 1, used_at = ? WHERE id = ?')
        .run(usedAt, 'rt-mark-used')
      
      const token = testDb.prepare('SELECT * FROM refresh_tokens WHERE id = ?').get('rt-mark-used')
      
      expect(token.used).toBe(1)
      expect(token.used_at).toBe(usedAt)
    })

    test('revokes refresh token', () => {
      const now = Date.now()
      const tokenHash = crypto.randomBytes(64).toString('hex')
      
      testDb.prepare(`
        INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('rt-revoke', 'user-rt-7', 'session-rt-7', tokenHash, now + 604800000, now)
      
      const result = testDb.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE id = ?').run('rt-revoke')
      
      expect(result.changes).toBe(1)
      
      const token = testDb.prepare('SELECT * FROM refresh_tokens WHERE id = ?').get('rt-revoke')
      expect(token.revoked).toBe(1)
    })

    test('revokes all session refresh tokens', () => {
      const now = Date.now()
      
      for (let i = 0; i < 3; i++) {
        testDb.prepare(`
          INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at)
          VALUES (?, ?, ?, ?, ?, ?)
        `).run(`rt-session-${i}`, 'user-rt-8', 'session-rt-8', `hash-${i}`, now + 604800000, now)
      }
      
      const result = testDb.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE session_id = ?').run('session-rt-8')
      
      expect(result.changes).toBe(3)
    })

    test('revokes all user refresh tokens', () => {
      const now = Date.now()
      
      for (let i = 0; i < 5; i++) {
        testDb.prepare(`
          INSERT INTO refresh_tokens (id, user_id, session_id, token_hash, expires_at, created_at)
          VALUES (?, ?, ?, ?, ?, ?)
        `).run(`rt-user-${i}`, 'user-rt-9', `session-${i}`, `hash-user-${i}`, now + 604800000, now)
      }
      
      const result = testDb.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?').run('user-rt-9')
      
      expect(result.changes).toBe(5)
    })
  })

  describe('Token Blacklist', () => {
    test('adds token to blacklist', () => {
      const now = Date.now()
      const jti = crypto.randomUUID()
      
      testDb.prepare(`
        INSERT INTO token_blacklist (id, token_jti, user_id, expires_at, created_at, reason)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run(crypto.randomUUID(), jti, 'user-bl-1', now + 3600000, now, 'logout')
      
      const entry = testDb.prepare('SELECT * FROM token_blacklist WHERE token_jti = ?').get(jti)
      
      expect(entry).toBeDefined()
      expect(entry.reason).toBe('logout')
    })

    test('checks if token is blacklisted', () => {
      const now = Date.now()
      const jti = crypto.randomUUID()
      
      testDb.prepare(`
        INSERT INTO token_blacklist (id, token_jti, user_id, expires_at, created_at)
        VALUES (?, ?, ?, ?, ?)
      `).run(crypto.randomUUID(), jti, 'user-bl-2', now + 3600000, now)
      
      const result = testDb.prepare('SELECT COUNT(*) as count FROM token_blacklist WHERE token_jti = ?').get(jti)
      
      expect(result.count).toBe(1)
    })

    test('token not blacklisted returns false', () => {
      const result = testDb.prepare('SELECT COUNT(*) as count FROM token_blacklist WHERE token_jti = ?').get('non-existent-jti')
      
      expect(result.count).toBe(0)
    })

    test('cleans up expired blacklist entries', () => {
      const now = Date.now()
      
      testDb.prepare(`
        INSERT INTO token_blacklist (id, token_jti, user_id, expires_at, created_at)
        VALUES (?, ?, ?, ?, ?)
      `).run(crypto.randomUUID(), 'jti-expired', 'user-bl-3', now - 1000, now)
      
      testDb.prepare(`
        INSERT INTO token_blacklist (id, token_jti, user_id, expires_at, created_at)
        VALUES (?, ?, ?, ?, ?)
      `).run(crypto.randomUUID(), 'jti-active', 'user-bl-3', now + 3600000, now)
      
      const result = testDb.prepare('DELETE FROM token_blacklist WHERE expires_at <= ?').run(now)
      
      expect(result.changes).toBe(1)
      
      const active = testDb.prepare('SELECT * FROM token_blacklist WHERE token_jti = ?').get('jti-active')
      expect(active).toBeDefined()
    })

    test('blacklist with different reasons', () => {
      const now = Date.now()
      const reasons = ['logout', 'password_change', 'security_breach', 'token_refresh']
      
      reasons.forEach((reason, i) => {
        testDb.prepare(`
          INSERT INTO token_blacklist (id, token_jti, user_id, expires_at, created_at, reason)
          VALUES (?, ?, ?, ?, ?, ?)
        `).run(crypto.randomUUID(), `jti-${i}`, `user-bl-4-${i}`, now + 3600000, now, reason)
      })
      
      const entries = testDb.prepare('SELECT * FROM token_blacklist WHERE user_id LIKE ?').all('user-bl-4-%')
      
      expect(entries.length).toBe(4)
    })
  })

  describe('Session Security', () => {
    test('enforces unique session tokens', () => {
      const now = Date.now()
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run('session-unique-1', 'user-sec-1', 'unique-token', now + 3600000, now, now)
      
      expect(() => {
        testDb.prepare(`
          INSERT INTO sessions (id, user_id, token, expires_at, created_at, last_accessed_at)
          VALUES (?, ?, ?, ?, ?, ?)
        `).run('session-unique-2', 'user-sec-2', 'unique-token', now + 3600000, now, now)
      }).toThrow()
    })

    test('stores device fingerprint', () => {
      const now = Date.now()
      const fingerprint = crypto.randomBytes(16).toString('hex')
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, device_fingerprint, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run('session-fp', 'user-sec-3', 'token-fp', fingerprint, now + 3600000, now, now)
      
      const session = testDb.prepare('SELECT * FROM sessions WHERE id = ?').get('session-fp')
      
      expect(session.device_fingerprint).toBe(fingerprint)
    })

    test('detects session from different device', () => {
      const now = Date.now()
      const fp1 = crypto.randomBytes(16).toString('hex')
      const fp2 = crypto.randomBytes(16).toString('hex')
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, device_fingerprint, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run('session-dev-1', 'user-sec-4', 'token-dev-1', fp1, now + 3600000, now, now)
      
      testDb.prepare(`
        INSERT INTO sessions (id, user_id, token, device_fingerprint, expires_at, created_at, last_accessed_at)
        VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run('session-dev-2', 'user-sec-4', 'token-dev-2', fp2, now + 3600000, now, now)
      
      const sessions = testDb.prepare('SELECT * FROM sessions WHERE user_id = ?').all('user-sec-4')
      
      expect(sessions.length).toBe(2)
      expect(sessions[0].device_fingerprint).not.toBe(sessions[1].device_fingerprint)
    })
  })
})
