'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const dbPath = path.join(os.tmpdir(), `bill-tracker-calendar-feed-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath;

const {
  buildCalendarFeed,
  createToken,
  escapeText,
  foldLine,
  rruleForCycle,
} = require('../services/calendarFeedService');
const { getDb, closeDb } = require('../db/database');

function createUser(db, username = 'calendar-user') {
  return db.prepare(`
    INSERT INTO users (username, password_hash, role, active, created_at, updated_at)
    VALUES (?, 'x', 'user', 1, datetime('now'), datetime('now'))
  `).run(username).lastInsertRowid;
}

function createBill(db, userId, overrides = {}) {
  return db.prepare(`
    INSERT INTO bills (
      user_id, name, due_day, expected_amount, cycle_type, cycle_day,
      billing_cycle, active, autopay_enabled, autodraft_status, created_at, updated_at
    )
    VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, 'none', datetime('now'), datetime('now'))
  `).run(
    userId,
    overrides.name || 'Water, Power; Internet',
    overrides.due_day || 15,
    overrides.expected_amount || 12345,
    overrides.cycle_type || 'monthly',
    overrides.cycle_day || '1',
    overrides.billing_cycle || 'monthly',
    overrides.autopay_enabled || 0,
  ).lastInsertRowid;
}

function callFeedRoute(query) {
  const router = require('../routes/calendarFeed');
  const layer = router.stack.find(item => item.route?.path === '/feed.ics' && item.route.methods.get);
  assert.ok(layer, 'GET /feed.ics route should exist');
  const handler = layer.route.stack[0].handle;

  return new Promise((resolve, reject) => {
    const req = {
      query,
    };
    const headers = {};
    const res = {
      statusCode: 200,
      body: '',
      headers,
      status(code) {
        this.statusCode = code;
        return this;
      },
      type(value) {
        headers['Content-Type'] = value;
        return this;
      },
      set(values) {
        Object.assign(headers, values);
        return this;
      },
      send(body) {
        this.body = body;
        resolve({ status: this.statusCode, headers, body });
      },
    };
    try {
      handler(req, res);
    } catch (err) {
      reject(err);
    }
  });
}

test.after(() => {
  closeDb();
  for (const suffix of ['', '-wal', '-shm']) {
    fs.rmSync(`${dbPath}${suffix}`, { force: true });
  }
});

test('escapeText follows RFC 5545 TEXT escaping', () => {
  assert.equal(
    escapeText('Comma, semicolon; slash\\ newline\nnext'),
    'Comma\\, semicolon\\; slash\\\\ newline\\nnext',
  );
});

test('foldLine folds at 75 octets without splitting UTF-8 characters', () => {
  const folded = foldLine(`SUMMARY:${'Long title '.repeat(12)}😀`);
  const physicalLines = folded.split('\r\n');

  assert.ok(physicalLines.length > 1);
  for (const line of physicalLines) {
    assert.ok(Buffer.byteLength(line, 'utf8') <= 75);
  }
  assert.ok(physicalLines.slice(1).every(line => line.startsWith(' ')));
  assert.equal(folded.replace(/\r\n /g, ''), `SUMMARY:${'Long title '.repeat(12)}😀`);
});

test('rruleForCycle maps all five bill cycle types', () => {
  assert.equal(rruleForCycle({ cycle_type: 'monthly', due_day: 12 }), 'FREQ=MONTHLY;BYMONTHDAY=12');
  assert.equal(rruleForCycle({ cycle_type: 'weekly', cycle_day: 'friday', due_day: 12 }), 'FREQ=WEEKLY;BYDAY=FR');
  assert.equal(rruleForCycle({ cycle_type: 'biweekly', cycle_day: 'monday', due_day: 12 }), 'FREQ=WEEKLY;INTERVAL=2;BYDAY=MO');
  assert.equal(rruleForCycle({ cycle_type: 'quarterly', cycle_day: '2', due_day: 28 }), 'FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=28');
  assert.equal(rruleForCycle({ cycle_type: 'annual', cycle_day: '11', due_day: 30 }), 'FREQ=YEARLY;BYMONTH=11;BYMONTHDAY=30');
});

test('buildCalendarFeed emits CRLF ICS without BOM or VTIMEZONE', () => {
  const db = getDb();
  const userId = createUser(db, 'feed-build');
  createBill(db, userId, { name: 'Rent' });

  const { ics, events } = buildCalendarFeed(userId, {
    now: new Date(Date.UTC(2026, 5, 7)),
    pastMonths: 0,
    futureMonths: 1,
    dtstamp: '20260607T120000Z',
  }, db);

  assert.ok(events.length >= 1);
  assert.equal(ics.charCodeAt(0), 'B'.charCodeAt(0));
  assert.ok(ics.includes('\r\n'));
  assert.ok(!ics.includes('\nBEGIN:VEVENT') || ics.includes('\r\nBEGIN:VEVENT'));
  assert.ok(!ics.includes('BEGIN:VTIMEZONE'));
  assert.match(ics, /DTSTART;VALUE=DATE:\d{8}\r\n/);
  assert.match(ics, /DTEND;VALUE=DATE:\d{8}\r\n/);
  assert.match(ics, /UID:bill-tracker-bill-\d+-\d{4}-\d{2}-\d{2}@bill-tracker\r\n/);
});

test('public feed endpoint serves ICS for valid token and rejects invalid tokens', async () => {
  const db = getDb();
  const userId = createUser(db, 'feed-http');
  createBill(db, userId, { name: 'Phone Plan', due_day: 8 });
  const token = createToken(userId, 'Test Feed', db);

  const missing = await callFeedRoute({ token: 'missing' });
  assert.equal(missing.status, 404);

  const response = await callFeedRoute({ token: token.token });
  assert.equal(response.status, 200);
  assert.equal(response.headers['Content-Type'], 'text/calendar; charset=utf-8');
  assert.ok(response.body.includes('BEGIN:VCALENDAR\r\n'));
  assert.ok(response.body.includes('SUMMARY:Phone Plan due\r\n'));

  const updated = db.prepare('SELECT last_used_at FROM calendar_tokens WHERE id = ?').get(token.id);
  assert.ok(updated.last_used_at);
});
