import * as crypto from 'node:crypto';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { KostalPlenticoreClient } from '../../ts/integrations/kostal_plenticore/index.js';

tap.test('authenticates to Kostal Plenticore REST API and reads/writes live data', async () => {
  const originalFetch = globalThis.fetch;
  const calls: Array<{ path: string; method: string; authorization?: string; body?: unknown }> = [];
  const password = 'secret-password';
  const user = 'user';
  const salt = Buffer.from('test-salt-for-pbkdf');
  const serverNonce = Buffer.from('server-nonce-12');
  const transactionId = Buffer.from('transaction-id-1');
  const rounds = 2;
  let clientNonce = Buffer.alloc(0);

  globalThis.fetch = (async (urlArg: URL | RequestInfo, initArg?: RequestInit) => {
    const url = new URL(String(urlArg));
    const body = parseBody(initArg?.body);
    const authorization = headerValue(initArg?.headers, 'authorization');
    calls.push({ path: url.pathname, method: initArg?.method || 'GET', authorization, body });

    if (url.pathname === '/api/v1/auth/start') {
      clientNonce = Buffer.from(String((body as Record<string, unknown>).nonce), 'base64');
      return jsonResponse({ nonce: b64(serverNonce), transactionId: b64(transactionId), salt: b64(salt), rounds });
    }
    if (url.pathname === '/api/v1/auth/finish') {
      return jsonResponse({ token: 'session-token', signature: b64(serverSignature(password, user, clientNonce, serverNonce, salt, rounds)) });
    }
    if (url.pathname === '/api/v1/auth/create_session') {
      expect(typeof (body as Record<string, unknown>).payload).toEqual('string');
      return jsonResponse({ sessionId: 'SESSION1' });
    }
    if (url.pathname === '/api/v1/info/version') {
      expect(authorization).toEqual('Session SESSION1');
      return jsonResponse({ hostname: 'kostal-host', name: 'Plenticore API', sw_version: 'FW1', api_version: '1' });
    }
    if (url.pathname === '/api/v1/processdata') {
      expect(authorization).toEqual('Session SESSION1');
      return jsonResponse([
        { moduleid: 'devices:local', processdata: [{ id: 'Inverter:State', unit: '', value: 6 }, { id: 'Dc_P', unit: 'W', value: 1234.4 }] },
        { moduleid: 'devices:local:pv1', processdata: [{ id: 'U', unit: 'V', value: 398.2 }] },
      ]);
    }
    if (url.pathname === '/api/v1/settings' && initArg?.method === 'POST') {
      expect(authorization).toEqual('Session SESSION1');
      return jsonResponse([
        {
          moduleid: 'devices:local',
          settings: [
            { id: 'Properties:SerialNo', value: 'SN123' },
            { id: 'Branding:ProductName1', value: 'PLENTICORE' },
            { id: 'Branding:ProductName2', value: 'plus' },
            { id: 'Properties:VersionIOC', value: 'IOC1' },
            { id: 'Properties:VersionMC', value: 'MC1' },
            { id: 'Battery:MinSoc', value: '10' },
            { id: 'Battery:SmartBatteryControl:Enable', value: '1' },
            { id: 'Battery:TimeControl:Enable', value: '0' },
          ],
        },
        { moduleid: 'scb:network', settings: [{ id: 'Hostname', value: 'Kostal Test' }] },
      ]);
    }
    if (url.pathname === '/api/v1/settings' && initArg?.method === 'PUT') {
      expect(authorization).toEqual('Session SESSION1');
      return jsonResponse({ ok: true });
    }
    if (url.pathname === '/api/v1/auth/logout') {
      return jsonResponse({ ok: true });
    }
    return jsonResponse({ message: 'not found' }, 404);
  }) as typeof globalThis.fetch;

  try {
    const client = new KostalPlenticoreClient({
      host: 'inverter.local',
      password,
      timeoutMs: 1000,
      processData: { 'devices:local': ['Inverter:State', 'Dc_P'], 'devices:local:pv1': ['U'] },
    });
    const snapshot = await client.getSnapshot(true);

    expect(snapshot.online).toBeTrue();
    expect(snapshot.source).toEqual('http');
    expect(snapshot.device.serialNumber).toEqual('SN123');
    expect(snapshot.entities.find((entityArg) => entityArg.id === 'devices_local_dc_p')?.state).toEqual(1234);
    expect(snapshot.entities.find((entityArg) => entityArg.id === 'devices_local_inverter_state')?.state).toEqual('FeedIn');
    expect(snapshot.entities.find((entityArg) => entityArg.id === 'battery_min_soc')?.state).toEqual(10);

    const write = await client.execute({ domain: 'number', service: 'set_value', target: {}, data: { moduleId: 'devices:local', dataId: 'Battery:MinSoc', value: 25 } });
    expect(write.success).toBeTrue();
    const settingsWrite = calls.find((callArg) => callArg.path === '/api/v1/settings' && callArg.method === 'PUT');
    expect(settingsWrite?.body).toEqual([{ moduleid: 'devices:local', settings: [{ id: 'Battery:MinSoc', value: '25' }] }]);
    expect(calls.some((callArg) => callArg.path === '/api/v1/auth/start')).toBeTrue();
    expect(calls.some((callArg) => callArg.path === '/api/v1/processdata')).toBeTrue();
    await client.destroy();
  } finally {
    globalThis.fetch = originalFetch;
  }
});

const serverSignature = (passwordArg: string, userArg: string, clientNonceArg: Buffer, serverNonceArg: Buffer, saltArg: Buffer, roundsArg: number): Buffer => {
  const saltedPassword = crypto.pbkdf2Sync(Buffer.from(passwordArg, 'utf8'), saltArg, roundsArg, 32, 'sha256');
  const serverKey = hmac(saltedPassword, 'Server Key');
  const authMessage = `n=${userArg},r=${b64(clientNonceArg)},r=${b64(serverNonceArg)},s=${b64(saltArg)},i=${roundsArg},c=biws,r=${b64(serverNonceArg)}`;
  return hmac(serverKey, authMessage);
};

const hmac = (keyArg: Buffer, messageArg: string): Buffer => crypto.createHmac('sha256', keyArg).update(Buffer.from(messageArg, 'utf8')).digest();

const b64 = (bufferArg: Buffer): string => bufferArg.toString('base64');

const jsonResponse = (valueArg: unknown, statusArg = 200): Response => new Response(JSON.stringify(valueArg), { status: statusArg, headers: { 'content-type': 'application/json' } });

const parseBody = (bodyArg: BodyInit | null | undefined): unknown => {
  if (typeof bodyArg === 'string') {
    return JSON.parse(bodyArg) as unknown;
  }
  return undefined;
};

const headerValue = (headersArg: HeadersInit | undefined, nameArg: string): string | undefined => {
  if (!headersArg) {
    return undefined;
  }
  if (headersArg instanceof Headers) {
    return headersArg.get(nameArg) || undefined;
  }
  if (Array.isArray(headersArg)) {
    return headersArg.find(([keyArg]) => keyArg.toLowerCase() === nameArg)?.[1];
  }
  return (headersArg as Record<string, string>)[nameArg];
};

export default tap.start();
