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-subscription-service-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath;

const { getDb, closeDb } = require('../db/database');
const {
  getSubscriptionRecommendations,
  recordSubscriptionFeedback,
  searchSubscriptionTransactions,
} = require('../services/subscriptionService');

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

function createTransaction(db, userId, overrides = {}) {
  return db.prepare(`
    INSERT INTO transactions
      (user_id, source_type, account_id, posted_date, amount, currency, description, payee, match_status, ignored)
    VALUES (?, 'manual', ?, ?, ?, 'USD', ?, ?, 'unmatched', 0)
  `).run(
    userId,
    overrides.account_id || null,
    overrides.posted_date || new Date().toISOString().slice(0, 10),
    overrides.amount ?? -1599,
    overrides.description || 'NETFLIX.COM',
    overrides.payee || 'NETFLIX.COM',
  ).lastInsertRowid;
}

function createAccount(db, userId, monitored = true) {
  const sourceId = db.prepare(`
    INSERT INTO data_sources (user_id, type, provider, name, status)
    VALUES (?, 'provider_sync', 'simplefin', 'SimpleFIN', 'active')
  `).run(userId).lastInsertRowid;

  return db.prepare(`
    INSERT INTO financial_accounts
      (user_id, data_source_id, provider_account_id, name, currency, monitored)
    VALUES (?, ?, ?, 'Checking', 'USD', ?)
  `).run(userId, sourceId, `acct-${sourceId}`, monitored ? 1 : 0).lastInsertRowid;
}

function createBill(db, userId, overrides = {}) {
  return db.prepare(`
    INSERT INTO bills
      (user_id, name, due_day, expected_amount, is_subscription, cycle_type, billing_cycle)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).run(
    userId,
    overrides.name || 'Netflix',
    overrides.due_day || 8,
    overrides.expected_amount ?? 1599,
    overrides.is_subscription ?? 1,
    overrides.cycle_type || 'monthly',
    overrides.billing_cycle || 'monthly',
  ).lastInsertRowid;
}

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

test('known catalog services appear as high-confidence subscription recommendations', () => {
  const db = getDb();
  const userId = createUser(db, 'recommendation');
  const accountId = createAccount(db, userId, true);
  createTransaction(db, userId, { account_id: accountId });

  const recommendations = getSubscriptionRecommendations(db, userId);
  const netflix = recommendations.find(item => item.catalog_match?.name === 'Netflix');

  assert.ok(netflix, 'Netflix catalog match should be recommended from one known charge');
  assert.equal(netflix.subscription_type, 'streaming');
  assert.equal(netflix.confidence >= 90, true);
  assert.deepEqual(netflix.accounts, ['Checking']);
  assert.match(netflix.reasons.join(' '), /Matches known service: Netflix/);
  assert.equal(netflix.evidence.identity.type, 'bank_descriptor');
  assert.equal(netflix.evidence.amount.match, 'monthly_plausible');
});

test('existing tracked bills are recommended for linking instead of tracking again', () => {
  const db = getDb();
  const userId = createUser(db, 'existing-bill');
  const accountId = createAccount(db, userId, true);
  const billId = createBill(db, userId, {
    name: 'Netflix',
    due_day: 12,
    expected_amount: 1599,
    is_subscription: 1,
  });
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'NETFLIX.COM',
    payee: 'NETFLIX.COM',
    amount: -1599,
    posted_date: '2026-04-12',
  });

  const recommendations = getSubscriptionRecommendations(db, userId);
  const netflix = recommendations.find(item => item.catalog_match?.name === 'Netflix');

  assert.ok(netflix, 'Netflix should still be recommended when an existing bill is present');
  assert.equal(netflix.recommended_action, 'link_existing_bill');
  assert.equal(netflix.existing_bill_match.id, billId);
  assert.equal(netflix.existing_bill_match.strong, true);
});

test('subscription recommendation feedback boosts future matching for the user', () => {
  const db = getDb();
  const userId = createUser(db, 'feedback-boost');
  const accountId = createAccount(db, userId, true);
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'NETFLIX.COM',
    payee: 'NETFLIX.COM',
    amount: -1599,
    posted_date: '2026-04-12',
  });

  const before = getSubscriptionRecommendations(db, userId).find(item => item.catalog_match?.name === 'Netflix');
  assert.ok(before, 'Netflix should be recommended before feedback');
  recordSubscriptionFeedback(db, userId, {
    action: 'accept_track_new',
    catalog_id: before.catalog_match.id,
    merchant: before.merchant,
    confidence: before.confidence,
  });

  const after = getSubscriptionRecommendations(db, userId).find(item => item.catalog_match?.name === 'Netflix');
  assert.ok(after, 'Netflix should still be recommended after feedback');
  assert.equal(after.confidence > before.confidence, true);
  assert.equal(after.evidence.feedback.positive_count > 0, true);
});

test('weak one-off known service names stay below the recommendation threshold', () => {
  const db = getDb();
  const userId = createUser(db, 'weak-known');
  const accountId = createAccount(db, userId, true);
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'MAX',
    payee: 'MAX',
    amount: -35000,
  });

  const recommendations = getSubscriptionRecommendations(db, userId);
  assert.equal(recommendations.some(item => item.catalog_match?.name === 'Max'), false);
});

test('broad Apple, Amazon, and Google one-off purchases with odd amounts are not recommended', () => {
  const db = getDb();
  const userId = createUser(db, 'broad-one-offs');
  const accountId = createAccount(db, userId, true);
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'APPLE.COM/BILL',
    payee: 'APPLE.COM/BILL',
    amount: -19999,
    posted_date: '2026-04-02',
  });
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'AMAZON MKTPLACE PMTS',
    payee: 'AMAZON MKTPLACE PMTS',
    amount: -8700,
    posted_date: '2026-04-03',
  });
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'GOOGLE *STORE',
    payee: 'GOOGLE *STORE',
    amount: -64999,
    posted_date: '2026-04-04',
  });

  const recommendations = getSubscriptionRecommendations(db, userId);
  const names = recommendations.map(item => item.catalog_match?.name || item.name).join(' ');
  assert.doesNotMatch(names, /Apple|Amazon|Google/);
});

test('annual known-service charges can be recommended when catalog annual pricing matches', () => {
  const db = getDb();
  const userId = createUser(db, 'annual-known');
  const accountId = createAccount(db, userId, true);
  db.prepare("UPDATE subscription_catalog SET starting_annual_usd = 191.88 WHERE name = 'Netflix'").run();
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'NETFLIX.COM',
    payee: 'NETFLIX.COM',
    amount: -19188,
    posted_date: '2026-04-12',
  });

  const recommendations = getSubscriptionRecommendations(db, userId);
  const netflix = recommendations.find(item => item.catalog_match?.name === 'Netflix');

  assert.ok(netflix, 'Annual Netflix charge should be recommended from exact descriptor and annual price');
  assert.equal(netflix.cycle_type, 'annual');
  assert.equal(netflix.evidence.amount.match, 'annual_close');
});

test('exact user descriptors produce high-confidence one-off recommendations', () => {
  const db = getDb();
  const userId = createUser(db, 'custom-descriptor');
  const accountId = createAccount(db, userId, true);
  const netflixCatalog = db.prepare("SELECT id FROM subscription_catalog WHERE name = 'Netflix'").get();
  db.prepare(`
    INSERT INTO user_catalog_descriptors (user_id, catalog_id, descriptor)
    VALUES (?, ?, 'NFX CUSTOM BILL')
  `).run(userId, netflixCatalog.id);
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'NFX CUSTOM BILL',
    payee: 'NFX CUSTOM BILL',
    amount: -1599,
    posted_date: '2026-04-12',
  });

  const recommendations = getSubscriptionRecommendations(db, userId);
  const netflix = recommendations.find(item => item.catalog_match?.name === 'Netflix');

  assert.ok(netflix, 'Custom descriptor should produce a Netflix recommendation');
  assert.equal(netflix.confidence >= 90, true);
  assert.equal(netflix.evidence.identity.type, 'user_descriptor');
});

test('ambiguous recurring known service names are flagged for review', () => {
  const db = getDb();
  const userId = createUser(db, 'ambiguous-known');
  const accountId = createAccount(db, userId, true);
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'MAX',
    payee: 'MAX',
    amount: -1099,
    posted_date: '2026-01-08',
  });
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'MAX',
    payee: 'MAX',
    amount: -1099,
    posted_date: '2026-02-08',
  });
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'MAX',
    payee: 'MAX',
    amount: -1099,
    posted_date: '2026-03-08',
  });

  const recommendations = getSubscriptionRecommendations(db, userId);
  const max = recommendations.find(item => item.catalog_match?.name === 'Max');

  assert.ok(max, 'Recurring Max charges should still be recommended');
  assert.equal(max.confidence >= 90, true);
  assert.equal(max.evidence.ambiguity.ambiguous, true);
  assert.equal(max.evidence.ambiguity.penalty > 0, true);
  assert.equal(max.transactions.length, 3);
});

test('unknown recurring patterns do not appear as known-service recommendations', () => {
  const db = getDb();
  const userId = createUser(db, 'unknown-pattern');
  const accountId = createAccount(db, userId, true);
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'LOCAL CLUB MEMBERSHIP',
    payee: 'LOCAL CLUB MEMBERSHIP',
    amount: -2500,
    posted_date: '2026-01-05',
  });
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'LOCAL CLUB MEMBERSHIP',
    payee: 'LOCAL CLUB MEMBERSHIP',
    amount: -2500,
    posted_date: '2026-02-05',
  });
  createTransaction(db, userId, {
    account_id: accountId,
    description: 'LOCAL CLUB MEMBERSHIP',
    payee: 'LOCAL CLUB MEMBERSHIP',
    amount: -2500,
    posted_date: '2026-03-05',
  });

  const recommendations = getSubscriptionRecommendations(db, userId);
  assert.equal(recommendations.some(item => item.merchant === 'local club membership'), false);
});

test('subscription transaction search annotates known catalog matches', () => {
  const db = getDb();
  const userId = createUser(db, 'search');
  const transactionId = createTransaction(db, userId, {
    description: 'NETFLIX.COM 866-579-7172',
    payee: 'NETFLIX.COM',
  });

  const matches = searchSubscriptionTransactions(db, userId, { q: 'netflix', limit: 10 });
  const match = matches.find(item => item.id === transactionId);

  assert.ok(match, 'transaction should be returned by subscription search');
  assert.equal(match.is_known_subscription, true);
  assert.equal(match.catalog_match.name, 'Netflix');
  assert.equal(match.catalog_match.subscription_type, 'streaming');
});

test('Claude.ai catalog seed matches Anthropic transaction descriptors', () => {
  const db = getDb();
  const userId = createUser(db, 'claude');
  const transactionId = createTransaction(db, userId, {
    description: 'ANTHROPIC CLAUDE PRO',
    payee: 'ANTHROPIC',
    amount: -2000,
  });

  const matches = searchSubscriptionTransactions(db, userId, { q: 'anthropic', limit: 10 });
  const match = matches.find(item => item.id === transactionId);

  assert.ok(match, 'Anthropic transaction should be returned by subscription search');
  assert.equal(match.is_known_subscription, true);
  assert.equal(match.catalog_match.name, 'Claude.ai');
  assert.equal(match.catalog_match.subscription_type, 'software');
});

test('subscription recommendations and search ignore unmonitored SimpleFIN accounts', () => {
  const db = getDb();
  const userId = createUser(db, 'unmonitored');
  const accountId = createAccount(db, userId, false);
  const transactionId = createTransaction(db, userId, {
    account_id: accountId,
    description: 'NETFLIX.COM 866-579-7172',
    payee: 'NETFLIX.COM',
  });

  const recommendations = getSubscriptionRecommendations(db, userId);
  assert.equal(recommendations.some(item => item.catalog_match?.name === 'Netflix'), false);

  const matches = searchSubscriptionTransactions(db, userId, { q: 'netflix', limit: 10 });
  assert.equal(matches.some(item => item.id === transactionId), false);
});
