const assert = require('assert');
const fs = require('fs');
const path = require('path');

const Monster = require('../src/specificClass');
const { MeasurementContainer } = require('generalFunctions');

function test(name, fn) {
  try {
    fn();
    console.log(`ok - ${name}`);
  } catch (err) {
    console.error(`not ok - ${name}`);
    console.error(err);
    process.exitCode = 1;
  }
}

function withMockedDate(iso, fn) {
  const RealDate = Date;
  let now = new RealDate(iso).getTime();

  class MockDate extends RealDate {
    constructor(...args) {
      if (args.length === 0) {
        super(now);
      } else {
        super(...args);
      }
    }

    static now() {
      return now;
    }
  }

  global.Date = MockDate;
  try {
    return fn({
      advance(ms) {
        now += ms;
      }
    });
  } finally {
    global.Date = RealDate;
  }
}

function buildConfig(overrides = {}) {
  return {
    general: {
      name: 'Monster Test',
      logging: { enabled: false, logLevel: 'error' }
    },
    asset: {
      emptyWeightBucket: 3
    },
    constraints: {
      samplingtime: 1,
      minVolume: 5,
      maxWeight: 23,
      nominalFlowMin: 1,
      flowMax: 10
    },
    ...overrides
  };
}

function parseMonsternametijdenCsv(filePath) {
  const raw = fs.readFileSync(filePath, 'utf8').trim();
  const lines = raw.split(/\r?\n/);
  const header = lines.shift();
  const columns = header.split(',');

  return lines
    .filter((line) => line && !line.startsWith('-----------'))
    .map((line) => {
      const parts = [];
      let cur = '';
      let inQ = false;
      for (let i = 0; i < line.length; i++) {
        const ch = line[i];
        if (ch === '"') {
          inQ = !inQ;
          continue;
        }
        if (ch === ',' && !inQ) {
          parts.push(cur);
          cur = '';
        } else {
          cur += ch;
        }
      }
      parts.push(cur);
      const obj = {};
      columns.forEach((col, idx) => {
        obj[col] = parts[idx];
      });
      return obj;
    });
}

test('measured + manual flow averages into effective flow', () => {
  withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
    const monster = new Monster(buildConfig());

    const child = {
      config: {
        general: { id: 'child-1', name: 'FlowSensor' },
        asset: { type: 'flow' }
      },
      measurements: new MeasurementContainer({
        autoConvert: true,
        defaultUnits: { flow: 'm3/h' }
      })
    };

    monster.registerChild(child, 'measurement');

    child.measurements
      .type('flow')
      .variant('measured')
      .position('downstream')
      .value(60, Date.now(), 'm3/h');

    monster.handleInput('input_q', { value: 20, unit: 'm3/h' });

    advance(1000);
    monster.tick();

    assert.strictEqual(monster.q, 40);
  });
});

test('invalid flow bounds prevent sampling start', () => {
  const monster = new Monster(buildConfig({
    constraints: {
      samplingtime: 1,
      minVolume: 5,
      maxWeight: 23,
      nominalFlowMin: 10,
      flowMax: 5
    }
  }));

  monster.handleInput('i_start', true);
  monster.sampling_program();

  assert.strictEqual(monster.invalidFlowBounds, true);
  assert.strictEqual(monster.running, false);
  assert.strictEqual(monster.i_start, false);
});

test('flowCalc uses elapsed time to compute m3PerTick', () => {
  withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
    const monster = new Monster(buildConfig());
    monster.q = 36; // m3/h

    monster.flowCalc();
    assert.strictEqual(monster.m3PerTick, 0);

    advance(10000);
    monster.flowCalc();

    const expected = 0.1; // 36 m3/h -> 0.01 m3/s over 10s
    assert.ok(Math.abs(monster.m3PerTick - expected) < 1e-6);
  });
});

test('prediction fallback uses nominalFlowMin * sampling_time when rain is stale', () => {
  const monster = new Monster(buildConfig());
  monster.nominalFlowMin = 4;
  monster.flowMax = 10;
  monster.rainMaxRef = 8;
  monster.sampling_time = 24;
  monster.lastRainUpdate = 0;
  const pred = monster.get_model_prediction();
  assert.strictEqual(pred, 96);
});

test('pulses increment when running with manual flow and zero nominalFlowMin', () => {
  withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
    const monster = new Monster(buildConfig({
      constraints: {
        samplingtime: 1,
        minVolume: 5,
        maxWeight: 23,
        nominalFlowMin: 0,
        flowMax: 6000,
        minSampleIntervalSec: 60,
        maxRainRef: 10
      }
    }));

    monster.handleInput('input_q', { value: 200, unit: 'm3/h' });
    monster.handleInput('i_start', true);

    for (let i = 0; i < 80; i++) {
      advance(1000);
      monster.tick();
    }

    assert.ok(monster.sumPuls > 0);
    assert.ok(monster.bucketVol > 0);
    assert.ok(monster.missedSamples > 0);
    assert.ok(monster.getSampleCooldownMs() > 0);
  });
});

test('rain data aggregation produces totals', () => {
  const monster = new Monster(buildConfig());
  const rainPath = path.join(__dirname, 'seed_data', 'raindataFormat.json');
  const rainData = JSON.parse(fs.readFileSync(rainPath, 'utf8'));

  monster.updateRainData(rainData);

  assert.ok(Object.keys(monster.aggregatedOutput).length > 0);
  assert.ok(monster.sumRain >= 0);
  assert.ok(monster.avgRain >= 0);
});

test('monsternametijden schedule sets next date', () => {
  withMockedDate('2024-10-15T00:00:00Z', () => {
    const monster = new Monster(buildConfig());
    const csvPath = path.join(__dirname, 'seed_data', 'monsternametijden.csv');
    const rows = parseMonsternametijdenCsv(csvPath);

    monster.aquonSampleName = '112100';
    monster.updateMonsternametijden(rows);

    const nextDate = monster.nextDate instanceof Date
      ? monster.nextDate.getTime()
      : Number(monster.nextDate);

    assert.ok(Number.isFinite(nextDate));
    assert.ok(nextDate > Date.now());
  });
});

test('output includes pulse and flow fields', () => {
  const monster = new Monster(buildConfig());
  const output = monster.getOutput();

  assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulse'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'q'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPuls'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPulse'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulsesRemaining'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulseFraction'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'flowToNextPulseM3'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'timeToNextPulseSec'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetVolumeM3'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetProgressPct'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetDeltaL'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetDeltaM3'));
  assert.ok(Object.prototype.hasOwnProperty.call(output, 'predictedRateM3h'));
});
