import assert from 'node:assert/strict';

function toContain(arrayLike, expected) {
  if (!Array.isArray(arrayLike) && typeof arrayLike !== 'string') {
    throw new assert.AssertionError({ message: 'toContain expects array or string' });
  }
  if (typeof arrayLike === 'string') {
    assert.equal(arrayLike.includes(String(expected)), true);
    return;
  }
  assert.equal(arrayLike.includes(expected), true);
}

function toContainEqual(arrayLike, expected) {
  assert.equal(Array.isArray(arrayLike), true);
  const found = arrayLike.some((entry) => {
    try {
      if (expected && expected.__matcher === 'objectContaining') {
        for (const [key, value] of Object.entries(expected.value || {})) {
          assert.deepEqual(entry?.[key], value);
        }
      } else {
        assert.deepEqual(entry, expected);
      }
      return true;
    } catch {
      return false;
    }
  });
  assert.equal(found, true);
}

function toHaveProperty(value, key) {
  assert.equal(value != null, true);
  assert.equal(Object.prototype.hasOwnProperty.call(value, key), true);
}

export function expect(actual) {
  const api = {
    toBe(expected) {
      assert.equal(actual, expected);
    },
    toBeDefined() {
      assert.notEqual(actual, undefined);
    },
    toBeNull() {
      assert.equal(actual, null);
    },
    toBeGreaterThan(expected) {
      assert.equal(Number(actual) > Number(expected), true);
    },
    toBeLessThan(expected) {
      assert.equal(Number(actual) < Number(expected), true);
    },
    toBeLessThanOrEqual(expected) {
      assert.equal(Number(actual) <= Number(expected), true);
    },
    toContain(expected) {
      toContain(actual, expected);
    },
    toContainEqual(expected) {
      toContainEqual(actual, expected);
    },
    toHaveProperty(key) {
      toHaveProperty(actual, key);
    },
    not: {
      toContain(expected) {
        if (typeof actual === 'string') {
          assert.equal(actual.includes(String(expected)), false);
          return;
        }
        assert.equal(Array.isArray(actual), true);
        assert.equal(actual.includes(expected), false);
      }
    }
  };

  Object.defineProperty(api, 'rejects', {
    get() {
      return {
        async toThrow(expectedMessage) {
          let thrown = null;
          try {
            await actual;
          } catch (error) {
            thrown = error;
          }
          assert.notEqual(thrown, null);
          if (expectedMessage !== undefined) {
            const text = String(thrown?.message || thrown || '');
            assert.equal(text.includes(String(expectedMessage)), true);
          }
        }
      };
    }
  });

  return api;
}

expect.objectContaining = function objectContaining(value) {
  return {
    __matcher: 'objectContaining',
    value
  };
};
