import json
import re
import unittest
from os import listdir
from os.path import dirname, isdir, isfile, join

from tests.helper import readJson


class PythonFile:
    def __init__(self, filename, path):
        self.filename = filename
        self.path = path


class TestForMissingProperties(unittest.TestCase):

    @unittest.skip("just for manual execution")
    def test_list_deprecated_properties(self):
        deprecated_features = self.read_all_deprecated_features()
        self.assertListEqual(deprecated_features.keys(), [], f"found deprecated features: {list(deprecated_features.keys())}")

    def test_deprecatedProperties(self):
        # with this test we want to check if deprecated properties are used in the code

        ignore = [
            # add deprecated properties here
            'heating.circuits.0.operating.programs.noDemand',
            'heating.circuits.0.operating.programs.summerEco',
            'ventilation.operating.programs.comfort',
            'ventilation.operating.programs.eco',
            'ventilation.operating.programs.holiday',
            'ventilation.operating.programs.levelFour',
            'ventilation.operating.programs.levelOne',
            'ventilation.operating.programs.levelThree',
            'ventilation.operating.programs.levelTwo',
            'ventilation.operating.programs.forcedLevelFour',
            'ventilation.operating.programs.silent',
            # Alternative naming conventions used as fallback for device compatibility
            'heating.buffer.sensors.temperature.main',
            'heating.buffer.sensors.temperature.top',
            'heating.dhw.sensors.temperature.hotWaterStorage',
            'heating.dhw.sensors.temperature.hotWaterStorage.top',
            'heating.dhw.sensors.temperature.hotWaterStorage.bottom',
            'heating.dhw.sensors.temperature.hotWaterStorage.middle',
            'heating.dhw.sensors.temperature.hotWaterStorage.midBottom',
            'heating.cop.green',  # deprecated, replaced by heating.cop.photovoltaic
        ]

        all_features = self.read_all_deprecated_features()
        all_python_files = self.read_all_python_code()
        deprecated_features = []
        for feature in all_features:
            if feature in ignore:
                continue
            if self.find_feature_in_code(all_python_files, feature):
                deprecated_features.append(feature)

        self.maxDiff = None
        self.assertListEqual([], deprecated_features, "found deprecated features in code")

    def test_missingProperties(self):
        # with this test we want to check if new properties
        # are added to the response files

        ignore = [
            # general - not yet used
            'device.messages.info.raw',
            'device.messages.service.raw',
            'device.messages.status.raw',
            'device.messages.errors.counter.d6',
            'device.messages.logbook',
            'device.parameterIdentification.version',
            'device.productIdentification',
            'device.productMatrix',
            'device.time.daylightSaving',
            'device.heatingCircuitId',
            'device.configuration.houseLocation',
            'device.lock.malfunction',
            'device.timeseries.burner.stops',
            'device.timeseries.dhw.burner.stops',
            'device.timeseries.ignitionTimeSteps',
            'device.timeseries.monitoringIonization',
            'device.timeseries.water.pressure.peaks',
            'device.information',
            'device.configuration.measurementWeight',
            'device.actorSensorTest',
            'device.brand',
            'device.lock.external',
            'device.power.consumption.limitation',
            'device.power.statusReport.consumption',
            'device.power.statusReport.production',
            'device.type',
            'device.variant',
            'heating.boiler.pumps.internal.current',
            'heating.boiler.temperature.current',
            'heating.compressors.0.heater.crankcase',
            'heating.configuration.heatingRod.dhw',
            'heating.configuration.heatingRod.heating',
            'heating.configuration.internalPumpOne',
            'heating.configuration.internalPumpTwo',
            'heating.configuration.temperature.outside.DampingFactor',
            'heating.economizers.0.sensors.temperature.liquid',
            'heating.evaporators.0.heater.base',
            'heating.heat.production.summary.cooling',
            'heating.heater.condensatePan',
            'heating.heater.fanRing',
            'heating.heatingRod',
            'heating.outdoor.defrosting',
            'heating.power.consumption.summary.cooling',
            'heating.primaryCircuit.fans.0.current',
            'heating.primaryCircuit.valves.fourThreeWay',
            'heating.secondaryCircuit.operation.state',
            'heating.secondaryCircuit.temperature.return.minimum',
            'heating.secondaryCircuit.valves.fourThreeWay',
            'heating.secondaryHeatGenerator',
            'heating.valves.fourThreeWay.position',

            'heating.boiler.pumps.internal',
            'heating.boiler.pumps.internal.target',
            'heating.burners.0.demand.temperature',
            'heating.calculated.temperature.outside',
            'heating.circuits.0.configuration.summerEco.absolute',
            'heating.configuration.bufferCylinderSize',
            'heating.configuration.centralHeatingCylinderSize',
            'heating.configuration.dhwCylinderPump',
            'heating.configuration.dhwCylinderSize',
            'heating.configuration.houseHeatingLoad',
            'heating.configuration.houseOrientation',
            'heating.configuration.internalPumps',
            'heating.configuration.pressure.total',
            'heating.dhw.scaldProtection',
            'heating.heat.production.summary.dhw',
            'heating.heat.production.summary.heating',

            # heating - not yet used
            'heating.operating.programs.holidayAtHome',
            'heating.operating.programs.holiday',
            'heating.device.time.offset',
            'heating.configuration.multiFamilyHouse',
            'heating.boiler.airflaps.0.position.current',
            'heating.boiler.airflaps.1.position.current',
            'heating.boiler.pumps.internal',
            'heating.boiler.pumps.internal.target',
            'heating.circuits.0.dhw.pumps.circulation.schedule',
            'heating.circuits.0.dhw.schedule',
            'heating.power.consumption.dhw',
            'heating.power.consumption',
            'heating.circuits.0.temperature.levels',  # hint: command
            'heating.dhw.hygiene',
            'heating.dhw.temperature',
            'heating.burners',
            'heating.sensors.temperature.allengra',
            'heating.sensors.valve.0.expansion.target',
            'heating.sensors.valve.1.expansion.target',
            'heating.dhw.hygiene.trigger',
            'heating.dhw.temperature.hygiene',
            'heating.dhw.operating.modes.off',
            'heating.dhw.operating.modes.active',
            'heating.dhw.operating.modes.comfort',
            'heating.dhw.operating.modes.eco',
            'heating.circuits.0.heating.roomInfluenceFactor',
            'heating.circuits.0.temperature',  # TODO: to analyse, from Vitodens 100W
            'heating.circuits.0.operating.programs.noDemand.hmiState',  # TODO: to analyse, from Vitodens 100W
            'heating.circuits.0.name',  # TODO: to analyse, from Vitodens 100W
            'heating.circuits.0.zone.mode',  # TODO: to analyse, from Vitocal 250A
            'heating.configuration.dhw.temperature.dhwCylinder.max',  # TODO: to analyse, from Vitocal 333G

            # Ignored for now as they are not documented in https://documentation.viessmann.com/static/iot/data-points
            'heating.device.software',

            # gateway

            # zigbee
            'device.zigbee.lqi',
            'device.zigbee.parent.rx',
            'device.zigbee.parent.tx',
            'device.zigbee.active',
            'device.zigbee.status',

            # fuel cell
            'fuelCell.electricalEnergyConsumption.value',
            'fuelCell.managers.energy.prediction.power.consumption',
            'fuelCell.managers.energy.prediction.runtime',
            'fuelCell.managers.energy.timeTillNextStart',
            'fuelCell.operating.phase',
            'fuelCell.prediction.heating.deficit',
            'fuelCell.sensors.temperature.return',
            'fuelCell.sensors.temperature.supply',
            'fuelCell.statistics',
            'heating.bufferCylinder.hysteresis',
            'heating.burners.0.preConditions',
            'heating.circuits.0.circulation.secondaryPump',
            'heating.dhw.pumps.secondary',
            'heating.increasedReturn.temperature',
            'heating.power.production.demandCoverage.current',
            'heating.power.production.productionCoverage.current',

            # heat pump
            'heating.circuits.0.cooling.hysteresis.switch',
            'heating.circuits.0.heating.hysteresis.switch',
            'heating.circuits.0.operating.programs.screedDrying.heatpump',
            'heating.compressors.0.heat.production.cooling.week',
            'heating.compressors.0.heat.production.dhw.week',
            'heating.compressors.0.heat.production.heating.week',
            'heating.compressors.0.heatTarget',
            'heating.compressors.0.power',
            'heating.compressors.0.power.consumption.dhw.week',
            'heating.compressors.0.power.consumption.heating.week',
            'heating.compressors.0.sensors.power',
            'heating.compressors.0.statistics.load',
            'heating.configuration.buffer.temperature.max',
            'heating.configuration.flow.temperature.max',
            'heating.configuration.flow.temperature.min',
            'heating.cop.cooling',
            'heating.cop.dhw',
            'heating.cop.green',  # deprecated, replaced by heating.cop.photovoltaic
            'heating.cop.heating',
            'heating.cop.total',
            'heating.heatingRod.heatTarget',
            'heating.heatingRod.runtime',
            'heating.primaryCircuit.sensors.rotation',
            'heating.sensors.pressure.hotGas',
            'heating.sensors.pressure.suctionGas',
            'heating.sensors.temperature.hotGas',
            'heating.sensors.temperature.liquidGas',
            'heating.sensors.temperature.suctionGas',
            'heating.heatingRod.status',
            'heating.scop.dhw', # deprecated
            'heating.scop.heating', # deprecated
            'heating.scop.total', # deprecated
            'heating.dhw.comfort', # deprecated

            # ventilation - not yet used
            'ventilation.control.filterChange',
            'ventilation.filter.pollution.blocked',
            'ventilation.levels.levelOne',
            'ventilation.levels.levelTwo',
            'ventilation.levels.levelThree',
            'ventilation.levels.levelFour',
            'ventilation.quickmodes.forcedLevelFour', # quickmode accessible via getVentilationQuickmode
            'ventilation.quickmodes.silent',
            'ventilation.quickmodes.standby',
            'ventilation.quickmodes.comfort',
            'ventilation.quickmodes.eco',
            'ventilation.quickmodes.holiday',
            # additional ventilation features seen in VitoairFs300E.json
            'device.commissioning.information',
            'ventilation.air.balance.offset',
            'ventilation.airQuality.co',
            'ventilation.airQuality.co2',
            'ventilation.airQuality.organicComponents',
            'ventilation.airQuality.pm10',
            'ventilation.airQuality.pm2d5',
            'ventilation.airQuality.temperature',
            'ventilation.bypass',
            'ventilation.bypass.configuration.temperature.perceived',
            'ventilation.bypass.configuration.temperature.supply.dynamicRegulation',
            'ventilation.bypass.configuration.temperature.supply.smoothRegulation',
            'ventilation.bypass.operating.modes.active',
            'ventilation.bypass.operating.modes.automatic',
            'ventilation.bypass.operating.modes.open',
            'ventilation.bypass.position',
            'ventilation.external.lock',
            'ventilation.fan.assignmentSwitch',
            'ventilation.fan.exhaust',
            'ventilation.fan.exhaust.runtime',
            'ventilation.features.co',
            'ventilation.features.co2',
            'ventilation.features.dust',
            'ventilation.features.finedust',
            'ventilation.features.organicComponent',
            'ventilation.filter.information',
            'ventilation.lockExternal',
            'ventilation.quickmodes.temporaryShutdown',
            'ventilation.sensors.actuator.selftest',
            'ventilation.switchActivation',

            # energy system - not yet used
            'device.etn',
            'device.serial.internalComponents',
            'ess.battery.usedAverage',
            'ess.configuration.backupBox',
            'ess.configuration.systemType',
            'ess.inverter.ac.power',
            'ess.sensors.temperature.ambient',
            'ess.version.hardware',
            'heating.device.mainECU',
            'pcc.ac.active.current',
            'pcc.ac.active.power',
            'pcc.ac.reactive.power',
            'pcc.state.gridCode',
            'photovoltaic.installedPeakPower',
            'photovoltaic.string.current',
            'photovoltaic.string.voltage',

            # FHT - not yet used
            'fht.configuration.floorCoolingCondensationShutdownMargin',
            'fht.configuration.floorCoolingCondensationThreshold',
            'fht.configuration.floorHeatingDamageProtectionThreshold',
            'fht.valve',

            # DEPRECATED - remove once data point is removed and test data is updated
            'heating.burner', # deprecated FIXME: remove once test data is updated
            'heating.buffer.hysteresis',
            'heating.buffer.sensors.temperature.main',
            'heating.configuration.houseLocation',
            'heating.dhw.sensors.temperature.hotWaterStorage',
            'heating.dhw.sensors.temperature.hotWaterStorage.bottom',
            'heating.dhw.sensors.temperature.hotWaterStorage.midBottom',
            'heating.dhw.sensors.temperature.hotWaterStorage.middle',
            'heating.dhw.sensors.temperature.hotWaterStorage.top',
            'heating.external.lock',
            'heating.fuelCell.electricalEnergyConsumption.value',
            'heating.fuelCell.managers.energy',
            'heating.fuelCell.managers.energy.prediction.power.consumption',
            'heating.fuelCell.managers.energy.prediction.runtime',
            'heating.fuelCell.managers.energy.timeTillNextStart',
            'heating.fuelCell.operating.phase',
            'heating.fuelCell.prediction.heating.deficit',
            'heating.fuelCell.sensors.temperature.return',
            'heating.fuelCell.sensors.temperature.supply',
            'heating.fuelCell.statistics',
            'heating.circuits.0.operating.programs.noDemand',
            'heating.dhw.comfort',
            'heating.circuits.0.operating.programs.summerEco',
            'heating.buffer.sensors.temperature.top',
            'heating.configuration.dhw.temperature.dhwCylinder.max',
            'ventilation.operating.programs.comfort',
            'ventilation.operating.programs.eco',
            'ventilation.operating.programs.holiday',
            'ventilation.operating.programs.levelFour',
            'ventilation.operating.programs.levelOne',
            'ventilation.operating.programs.levelThree',
            'ventilation.operating.programs.levelTwo',
            'heating.configuration.dhw.highDemand.threshold',
            'heating.configuration.dhw.highDemand.timeframe',
            'heating.configuration.dhw.temperature.comfortCharging',
            'heating.device.variant',
            'heating.noise.reduction.operating.programs.active',
            'heating.noise.reduction.operating.programs.maxReduced',
            'heating.noise.reduction.operating.programs.notReduced',
            'heating.noise.reduction.operating.programs.slightlyReduced',
            'heating.scop.dhw',
            'heating.scop.heating',
            'heating.scop.total',
            'heating.configuration.dhw.temperature.hotWaterStorage.max',
            'heating.configuration.gasType',
            'ventilation.sensors.airQuality',
            'ventilation.operating.programs.forcedLevelFour',
            'ventilation.operating.programs.silent',
            # Vitoset Aqua water softener: device-level status not yet exposed
            'device.status',
        ]

        all_features = self.read_all_features()
        all_python_files = self.read_all_python_code()

        missing_features = {}
        for feature in all_features:
            found = self.find_feature_in_code(all_python_files, feature)

            foundInFiles = all_features[feature]['files']
            if not found and len(foundInFiles) > 0 and feature not in ignore:
                missing_features[feature] = foundInFiles

        self.maxDiff = None
        self.assertDictEqual({}, missing_features, "found new data points")

    def test_unverifiedProperties(self):
        # with this test we want to verify if we access
        # properties which are not in any test response data

        ignore = [
            'heating.dhw.sensors.temperature.dhwCylinder.midBottom',  # FIXME: remove once test data is updated
            'ventilation.quickmodes',
            'heating.heatingRod.heat.production.current',
            'heating.heatingRod.power.consumption.current',
            'heating.heatingRod.power.consumption.heating',
            'heating.heatingRod.power.consumption.dhw',
            'heating.heatingRod.power.consumption.total',
            'heating.compressors.0.power.consumption.current',
            'heating.compressors.0.power.consumption.heating',
            'heating.compressors.0.heat.production.current',
            'heating.compressors.0.power.consumption.cooling',
            'heating.compressors.0.power.consumption.dhw',
            'heating.compressors.0.power.consumption.total',
            'ventilation.sensors.temperature.outside',
            'ventilation.sensors.humidity.outdoor',
            'heating.compressors.0.sensors.pressure.outlet',
        ]

        all_features = self.read_all_features()
        all_python_files = self.read_all_python_code()

        used_features = []
        for python in all_python_files:
            if python in ['PyViCareFuelCell.py']:  # skip, where we miss test data
                continue

            for match in re.findall(r'getProperty\(\s*?f?"(.*)"\s*?\)', all_python_files[python]):
                feature_name = re.sub(r'{(self\.)?(circuit|burner|compressor|condensor|evaporator|inverter|room_id|slot)}', '0', match)
                feature_name = re.sub(r'{burner}', '0', feature_name)
                feature_name = re.sub(r'{circuit}', '0', feature_name)  # for local variable in loops
                feature_name = re.sub(r'\.{(quickmode|mode|program|active_program)}', '', feature_name)
                used_features.append(feature_name)

        self.maxDiff = None
        self.assertSetEqual(set([]), set(used_features) - set(all_features) - set(ignore), "found untested data points")

    def find_feature_in_code(self, all_python_files, feature):
        search_string = f'[\'"]{feature}[\'"]'.replace(".", r"\.")
        search_string = re.sub(r"\b\d\b", r"{.*?}", search_string)
        search_string = re.sub(r'\\.modes\\.\w+', r'\\.modes\\.\\w+', search_string)
        search_string = re.sub(r'\\.programs\\.\w+', r'\\.programs\\.\\w+', search_string)

        found = False
        for search_python in all_python_files:
            if re.search(search_string, all_python_files[search_python]):
                found = True
                break
        return found

    def read_all_python_code(self):
        python_path = join(dirname(__file__), '../PyViCare')
        # searches in all subdirectories
        python_files = self.get_all_files(python_path)

        all_python_files = {}

        for python in python_files:
            if not python.filename.endswith(".py"):
                continue

            with open(join(python.path, python.filename)) as f:
                all_python_files[python.filename] = f.read()

        return all_python_files

    def get_all_files(self, path):
        files = []
        for f in listdir(path):
            new_path = join(path, f)
            if isdir(new_path):
                files.extend(self.get_all_files(new_path))
            elif isfile(new_path):
                files.append(PythonFile(f, path))

        return files

    def read_all_deprecated_features(self):
        response_path = join(dirname(__file__), './response')
        response_files = [f for f in listdir(response_path) if isfile(join(response_path, f))]

        all_features = {}

        # Load from deprecation database (maintained by check_deprecations.py)
        db_path = join(dirname(__file__), 'deprecated_features.json')
        if isfile(db_path):
            with open(db_path) as f:
                db = json.load(f)
            for name, info in db.get('features', {}).items():
                normalized = re.sub(r"\b\d\b", "0", name)
                if normalized not in all_features:
                    all_features[normalized] = {'files': info.get('sources', [])}

        # Also scan test response files directly (catches new deprecations not yet in db)
        for response in response_files:
            data = readJson(join(response_path, response))
            if "data" in data:
                for feature in data["data"]:
                    if "deprecated" in feature and feature["deprecated"]:
                        name = re.sub(r"\b\d\b", "0", feature["feature"])
                        if name not in all_features:
                            all_features[name] = {'files': []}
                        all_features[name]['files'].append(response)

        return all_features

    def read_all_features(self):
        response_path = join(dirname(__file__), './response')
        response_files = [f for f in listdir(response_path) if isfile(join(response_path, f))]

        all_features = {}
        for response in response_files:
            data = readJson(join(response_path, response))
            if "data" in data:
                for feature in data["data"]:
                    name = re.sub(r"\b\d\b", "0", feature["feature"])
                    if name not in all_features:
                        all_features[name] = {'files': []}

                    if feature['isEnabled'] and feature['properties'] != {}:
                        all_features[name]['files'].append(response)
        return all_features
